From 18504ff2cb3945a94f86d8cfbc1bf581c83bab38 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Thu, 28 Sep 2023 14:12:11 -0400 Subject: [PATCH 001/108] Update `agg_tiles_hash` docs and minor bug (#901) * Do not open the same mbtiles file more than once at the same time: reuse the (unsafe) sqlite handle to create a `RusqliteConnection` instead. * The copying should set `agg_tiles_hash` in all cases because now it uses the always available `tiles` table/view. * a few minor cleanups and renames related to that --- .github/workflows/ci.yml | 2 +- Cargo.lock | 4 +- docs/src/tools.md | 103 +++--- ...3c46f61ff92ffbc6ec3bba4860abd60d224cb.json | 12 + ...7e779ecf324e1862945fbd18da4bf5baf565b.json | 12 + ...2ee47cfc72b56f6ed275a0b0688047405498f.json | 12 + ...1f52ce710d8978e3b35b59b724fc5bee9f55c.json | 12 + martin-mbtiles/src/bin/main.rs | 10 +- martin-mbtiles/src/lib.rs | 4 +- martin-mbtiles/src/mbtiles.rs | 119 ++++--- martin-mbtiles/src/tile_copier.rs | 319 +++++++++--------- 11 files changed, 346 insertions(+), 263 deletions(-) create mode 100644 martin-mbtiles/.sqlx/query-45de99a3628a53940ef80b0e2603c46f61ff92ffbc6ec3bba4860abd60d224cb.json create mode 100644 martin-mbtiles/.sqlx/query-a115609880b2c6ed3beeb5aaf8c7e779ecf324e1862945fbd18da4bf5baf565b.json create mode 100644 martin-mbtiles/.sqlx/query-d1d61dfa7c34dafb4588f78e23b2ee47cfc72b56f6ed275a0b0688047405498f.json create mode 100644 martin-mbtiles/.sqlx/query-e13e2e17d5bf56287bc0fd7c55a1f52ce710d8978e3b35b59b724fc5bee9f55c.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fa5bd6473..39678e3cb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -506,7 +506,7 @@ jobs: elif [[ "${{ matrix.target }}" == "debian-x86_64" ]]; then mv debian-x86_64.deb ../${{ matrix.name }} else - tar czvf ../${{ matrix.name }} martin${{ matrix.ext }} mbtiles${{ matrix.ext }} + tar czvf ../${{ matrix.name }} martin${{ matrix.ext }} mbtiles${{ matrix.ext }} fi - name: Generate SHA-256 (MacOS) if: matrix.sha == 'true' diff --git a/Cargo.lock b/Cargo.lock index fbdfe457a..097c9d88e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2978,9 +2978,9 @@ dependencies = [ [[package]] name = "sqlite-hashes" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f756a7c1f66e2d70c9acb5881776ba0ae25ba2aaf68e2f69ed32d96c42313fab" +checksum = "fd203121770e67b5f689ebf9592c88d3529193743f35630413f419be8ef1e835" dependencies = [ "digest", "md-5", diff --git a/docs/src/tools.md b/docs/src/tools.md index 05f444035..a2e35b5e0 100644 --- a/docs/src/tools.md +++ b/docs/src/tools.md @@ -69,47 +69,64 @@ If the `.mbtiles` file is of `flat_with_hash` or `normalized` type, then verify mbtiles validate src_file.mbtiles ``` +## Content Validation +The original [MBTiles specification](https://github.com/mapbox/mbtiles-spec#readme) does not provide any guarantees for the content of the tile data in MBTiles. This tool adds a few additional conventions to ensure that the content of the tile data is valid. + +A typical Normalized schema generated by tools like [tilelive-copy](https://github.com/mapbox/TileLive#bintilelive-copy) use MD5 hash in the `tile_id` column. The Martin's `mbtiles` tool can use this hash to verify the content of each tile. We also define a new `flat-with-hash` schema that stores the hash and tile data in the same table. This schema is more efficient than the `normalized` schema when data has no duplicate tiles (see below). Per tile validation is not available for `flat` schema. + +Per-tile validation will catch individual invalid tiles, but it will not detect overall datastore corruption (e.g. missing tiles or tiles that shouldn't exist, or tiles with incorrect z/x/y values). For that, Martin `mbtiles` tool defines a new metadata value called `agg_tiles_hash`. The value is computed by hashing `cast(zoom_level AS text), cast(tile_column AS text), cast(tile_row AS text), cast(tile_data as blob)` combined for all rows in the `tiles` table/view, ordered by z,x,y. In case there are no rows or all are NULL, the hash value of an empty string is used. + +The `mbtiles` tool will compute `agg_tiles_hash` value when copying or validating mbtiles files. + ## Supported Schema -The `mbtiles` tool supports three different kinds of schema for `tiles` data in `.mbtiles` files: - -- `flat`: - ``` - CREATE TABLE tiles (zoom_level integer, tile_column integer, tile_row integer, tile_data blob); - CREATE UNIQUE INDEX tile_index on tiles (zoom_level, tile_column, tile_row); - ``` -- `flat-with-hash`: - ``` - CREATE TABLE tiles_with_hash (zoom_level integer NOT NULL, tile_column integer NOT NULL, tile_row integer NOT NULL, tile_data blob, tile_hash text); - CREATE UNIQUE INDEX tiles_with_hash_index on tiles_with_hash (zoom_level, tile_column, tile_row); - CREATE VIEW tiles AS SELECT zoom_level, tile_column, tile_row, tile_data FROM tiles_with_hash; - ``` -- `normalized`: - ``` - CREATE TABLE map (zoom_level INTEGER, tile_column INTEGER, tile_row INTEGER, tile_id TEXT); - CREATE UNIQUE INDEX map_index ON map (zoom_level, tile_column, tile_row); - CREATE TABLE images (tile_data blob, tile_id text); - CREATE UNIQUE INDEX images_id ON images (tile_id); - CREATE VIEW tiles AS - SELECT - map.zoom_level AS zoom_level, - map.tile_column AS tile_column, - map.tile_row AS tile_row, - images.tile_data AS tile_data - FROM map - JOIN images ON images.tile_id = map.tile_id; - ``` - Optionally, `.mbtiles` files with `normalized` schema can include a `tiles_with_hash` view: - ``` - CREATE VIEW tiles_with_hash AS - SELECT - map.zoom_level AS zoom_level, - map.tile_column AS tile_column, - map.tile_row AS tile_row, - images.tile_data AS tile_data, - images.tile_id AS tile_hash - FROM map - JOIN images ON images.tile_id = map.tile_id; - ``` - **__Note:__** All `normalized` files created by the `mbtiles` tool will contain this view. - -For more general spec information, see [here](https://github.com/mapbox/mbtiles-spec#readme). +The `mbtiles` tool supports three different kinds of schema for `tiles` data in `.mbtiles` files. See also the original [specification](https://github.com/mapbox/mbtiles-spec#readme). + +#### flat +```sql, ignore +CREATE TABLE tiles (zoom_level integer, tile_column integer, tile_row integer, tile_data blob); +CREATE UNIQUE INDEX tile_index on tiles (zoom_level, tile_column, tile_row); +``` + +#### flat-with-hash +```sql, ignore +CREATE TABLE tiles_with_hash ( + zoom_level integer NOT NULL, + tile_column integer NOT NULL, + tile_row integer NOT NULL, + tile_data blob, + tile_hash text); +CREATE UNIQUE INDEX tiles_with_hash_index on tiles_with_hash (zoom_level, tile_column, tile_row); +CREATE VIEW tiles AS SELECT zoom_level, tile_column, tile_row, tile_data FROM tiles_with_hash; +``` + +#### normalized +```sql, ignore +CREATE TABLE map (zoom_level INTEGER, tile_column INTEGER, tile_row INTEGER, tile_id TEXT); +CREATE UNIQUE INDEX map_index ON map (zoom_level, tile_column, tile_row); +CREATE TABLE images (tile_data blob, tile_id text); +CREATE UNIQUE INDEX images_id ON images (tile_id); +CREATE VIEW tiles AS + SELECT + map.zoom_level AS zoom_level, + map.tile_column AS tile_column, + map.tile_row AS tile_row, + images.tile_data AS tile_data + FROM map + JOIN images ON images.tile_id = map.tile_id; +``` + +Optionally, `.mbtiles` files with `normalized` schema can include a `tiles_with_hash` view: + +```sql, ignore +CREATE VIEW tiles_with_hash AS + SELECT + map.zoom_level AS zoom_level, + map.tile_column AS tile_column, + map.tile_row AS tile_row, + images.tile_data AS tile_data, + images.tile_id AS tile_hash + FROM map + JOIN images ON images.tile_id = map.tile_id; +``` + +**__Note:__** All `normalized` files created by the `mbtiles` tool will contain this view. diff --git a/martin-mbtiles/.sqlx/query-45de99a3628a53940ef80b0e2603c46f61ff92ffbc6ec3bba4860abd60d224cb.json b/martin-mbtiles/.sqlx/query-45de99a3628a53940ef80b0e2603c46f61ff92ffbc6ec3bba4860abd60d224cb.json new file mode 100644 index 000000000..7f4b7b65a --- /dev/null +++ b/martin-mbtiles/.sqlx/query-45de99a3628a53940ef80b0e2603c46f61ff92ffbc6ec3bba4860abd60d224cb.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "ATTACH DATABASE ? AS srcDb", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "45de99a3628a53940ef80b0e2603c46f61ff92ffbc6ec3bba4860abd60d224cb" +} diff --git a/martin-mbtiles/.sqlx/query-a115609880b2c6ed3beeb5aaf8c7e779ecf324e1862945fbd18da4bf5baf565b.json b/martin-mbtiles/.sqlx/query-a115609880b2c6ed3beeb5aaf8c7e779ecf324e1862945fbd18da4bf5baf565b.json new file mode 100644 index 000000000..fc0b3c0c2 --- /dev/null +++ b/martin-mbtiles/.sqlx/query-a115609880b2c6ed3beeb5aaf8c7e779ecf324e1862945fbd18da4bf5baf565b.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "ATTACH DATABASE ? AS newDb", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "a115609880b2c6ed3beeb5aaf8c7e779ecf324e1862945fbd18da4bf5baf565b" +} diff --git a/martin-mbtiles/.sqlx/query-d1d61dfa7c34dafb4588f78e23b2ee47cfc72b56f6ed275a0b0688047405498f.json b/martin-mbtiles/.sqlx/query-d1d61dfa7c34dafb4588f78e23b2ee47cfc72b56f6ed275a0b0688047405498f.json new file mode 100644 index 000000000..1d9e5c432 --- /dev/null +++ b/martin-mbtiles/.sqlx/query-d1d61dfa7c34dafb4588f78e23b2ee47cfc72b56f6ed275a0b0688047405498f.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "ATTACH DATABASE ? AS originalDb", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "d1d61dfa7c34dafb4588f78e23b2ee47cfc72b56f6ed275a0b0688047405498f" +} diff --git a/martin-mbtiles/.sqlx/query-e13e2e17d5bf56287bc0fd7c55a1f52ce710d8978e3b35b59b724fc5bee9f55c.json b/martin-mbtiles/.sqlx/query-e13e2e17d5bf56287bc0fd7c55a1f52ce710d8978e3b35b59b724fc5bee9f55c.json new file mode 100644 index 000000000..5d8f76197 --- /dev/null +++ b/martin-mbtiles/.sqlx/query-e13e2e17d5bf56287bc0fd7c55a1f52ce710d8978e3b35b59b724fc5bee9f55c.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "ATTACH DATABASE ? AS diffDb", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "e13e2e17d5bf56287bc0fd7c55a1f52ce710d8978e3b35b59b724fc5bee9f55c" +} diff --git a/martin-mbtiles/src/bin/main.rs b/martin-mbtiles/src/bin/main.rs index 7d8721a05..39579703b 100644 --- a/martin-mbtiles/src/bin/main.rs +++ b/martin-mbtiles/src/bin/main.rs @@ -2,9 +2,7 @@ use std::path::{Path, PathBuf}; use anyhow::Result; use clap::{Parser, Subcommand}; -use martin_mbtiles::{ - apply_mbtiles_diff, copy_mbtiles_file, IntegrityCheckType, Mbtiles, TileCopierOptions, -}; +use martin_mbtiles::{apply_mbtiles_diff, IntegrityCheckType, Mbtiles, TileCopierOptions}; use sqlx::sqlite::SqliteConnectOptions; use sqlx::{Connection, SqliteConnection}; @@ -67,7 +65,7 @@ enum Commands { /// Value to specify the extent of the SQLite integrity check performed #[arg(long, value_enum, default_value_t=IntegrityCheckType::default())] integrity_check: IntegrityCheckType, - /// Generate a hash of the tile data hashes and store under the 'agg_tiles_hash' key in metadata + /// Update `agg_tiles_hash` metadata value instead of using it to validate if the entire tile store is valid. #[arg(long)] update_agg_tiles_hash: bool, }, @@ -88,7 +86,7 @@ async fn main() -> Result<()> { meta_set_value(file.as_path(), &key, value).await?; } Commands::Copy(opts) => { - copy_mbtiles_file(opts).await?; + opts.run().await?; } Commands::ApplyDiff { src_file, @@ -148,7 +146,7 @@ async fn validate_mbtiles( if update_agg_tiles_hash { mbt.update_agg_tiles_hash(&mut conn).await?; } else { - mbt.check_agg_tile_hashes(&mut conn).await?; + mbt.check_agg_tiles_hashes(&mut conn).await?; } Ok(()) } diff --git a/martin-mbtiles/src/lib.rs b/martin-mbtiles/src/lib.rs index 9c16049ef..4d90e4231 100644 --- a/martin-mbtiles/src/lib.rs +++ b/martin-mbtiles/src/lib.rs @@ -9,6 +9,4 @@ mod tile_copier; pub use errors::MbtError; pub use mbtiles::{IntegrityCheckType, Mbtiles, Metadata}; pub use mbtiles_pool::MbtilesPool; -pub use tile_copier::{ - apply_mbtiles_diff, copy_mbtiles_file, CopyDuplicateMode, TileCopierOptions, -}; +pub use tile_copier::{apply_mbtiles_diff, CopyDuplicateMode, TileCopierOptions}; diff --git a/martin-mbtiles/src/mbtiles.rs b/martin-mbtiles/src/mbtiles.rs index 4912e4e72..b37cbd8df 100644 --- a/martin-mbtiles/src/mbtiles.rs +++ b/martin-mbtiles/src/mbtiles.rs @@ -15,11 +15,8 @@ use serde::ser::SerializeStruct; use serde::Serialize; use serde_json::{Value as JSONValue, Value}; use sqlite_hashes::register_md5_function; -use sqlite_hashes::rusqlite::{ - Connection as RusqliteConnection, Connection, OpenFlags, OptionalExtension, -}; -use sqlx::sqlite::SqliteRow; -use sqlx::{query, Row, SqliteExecutor}; +use sqlx::sqlite::{SqliteConnectOptions, SqliteRow}; +use sqlx::{query, Connection as _, Row, SqliteConnection, SqliteExecutor}; use tilejson::{tilejson, Bounds, Center, TileJSON}; use crate::errors::{MbtError, MbtResult}; @@ -96,6 +93,15 @@ impl Mbtiles { }) } + pub async fn open_with_hashes(&self, readonly: bool) -> MbtResult { + let opt = SqliteConnectOptions::new() + .filename(self.filepath()) + .read_only(readonly); + let mut conn = SqliteConnection::connect_with(&opt).await?; + attach_hash_fn(&mut conn).await?; + Ok(conn) + } + #[must_use] pub fn filepath(&self) -> &str { &self.filepath @@ -420,41 +426,6 @@ impl Mbtiles { Err(MbtError::NoUniquenessConstraint(self.filepath.clone())) } - /// Compute the hash of the combined tiles in the mbtiles file tiles table/view. - /// This should work on all mbtiles files perf `MBTiles` specification. - fn calc_agg_tiles_hash(&self) -> MbtResult { - Ok(self.open_with_hashes(true)?.query_row_and_then( - // The md5_concat func will return NULL if there are no rows in the tiles table. - // For our use case, we will treat it as an empty string, and hash that. - "SELECT hex( - coalesce( - md5_concat( - cast(zoom_level AS text), - cast(tile_column AS text), - cast(tile_row AS text), - tile_data - ), - md5('') - ) - ) - FROM tiles - ORDER BY zoom_level, tile_column, tile_row;", - [], - |row| row.get(0), - )?) - } - - pub(crate) fn open_with_hashes(&self, is_readonly: bool) -> MbtResult { - let flags = if is_readonly { - OpenFlags::SQLITE_OPEN_READ_ONLY - } else { - OpenFlags::default() - }; - let rusqlite_conn = RusqliteConnection::open_with_flags(self.filepath(), flags)?; - register_md5_function(&rusqlite_conn)?; - Ok(rusqlite_conn) - } - /// Perform `SQLite` internal integrity check pub async fn check_integrity( &self, @@ -491,7 +462,7 @@ impl Mbtiles { Ok(()) } - pub async fn check_agg_tile_hashes(&self, conn: &mut T) -> MbtResult<()> + pub async fn check_agg_tiles_hashes(&self, conn: &mut T) -> MbtResult<()> where for<'e> &'e mut T: SqliteExecutor<'e>, { @@ -499,7 +470,8 @@ impl Mbtiles { return Err(AggHashValueNotFound(self.filepath().to_string())); }; - let computed = self.calc_agg_tiles_hash()?; + // let conn = self.open_with_hashes(true)?; + let computed = calc_agg_tiles_hash(&mut *conn).await?; if stored != computed { let file = self.filepath().to_string(); return Err(AggHashMismatch(computed, stored, file)); @@ -514,7 +486,7 @@ impl Mbtiles { for<'e> &'e mut T: SqliteExecutor<'e>, { let old_hash = self.get_agg_tiles_hash(&mut *conn).await?; - let hash = self.calc_agg_tiles_hash()?; + let hash = calc_agg_tiles_hash(&mut *conn).await?; if old_hash.as_ref() == Some(&hash) { info!( "agg_tiles_hash is already set to the correct value `{hash}` in {}", @@ -570,31 +542,70 @@ impl Mbtiles { } }; - self.open_with_hashes(true)? - .query_row_and_then(sql, [], |r| Ok((r.get(0)?, r.get(1)?))) - .optional()? - .map_or(Ok(()), |v: (String, String)| { - Err(IncorrectTileHash(self.filepath().to_string(), v.0, v.1)) + query(sql) + .fetch_optional(&mut *conn) + .await? + .map_or(Ok(()), |v| { + Err(IncorrectTileHash( + self.filepath().to_string(), + v.get(0), + v.get(1), + )) }) } } +/// Compute the hash of the combined tiles in the mbtiles file tiles table/view. +/// This should work on all mbtiles files perf `MBTiles` specification. +async fn calc_agg_tiles_hash(conn: &mut T) -> MbtResult +where + for<'e> &'e mut T: SqliteExecutor<'e>, +{ + let query = query( + // The md5_concat func will return NULL if there are no rows in the tiles table. + // For our use case, we will treat it as an empty string, and hash that. + // Note that in some weird rare cases, a column with blob type may be stored as an integer value + "SELECT + hex( + coalesce( + md5_concat( + cast(zoom_level AS text), + cast(tile_column AS text), + cast(tile_row AS text), + cast(tile_data as blob) + ), + md5('') + ) + ) + FROM tiles + ORDER BY zoom_level, tile_column, tile_row;", + ); + Ok(query.fetch_one(conn).await?.get::(0)) +} + +pub async fn attach_hash_fn(conn: &mut SqliteConnection) -> MbtResult<()> { + let handle = conn.lock_handle().await?.as_raw_handle().as_ptr(); + // Safety: we know that the handle is a SQLite connection is locked and is not used anywhere else. + // The registered functions will be dropped when SQLX drops DB connection. + let rc = unsafe { sqlite_hashes::rusqlite::Connection::from_handle(handle) }?; + register_md5_function(&rc)?; + Ok(()) +} + #[cfg(test)] mod tests { use std::collections::HashMap; use martin_tile_utils::Encoding; - use sqlx::{Connection, SqliteConnection}; use tilejson::VectorLayer; use super::*; async fn open(filepath: &str) -> (SqliteConnection, Mbtiles) { let mbt = Mbtiles::new(filepath).unwrap(); - ( - SqliteConnection::connect(mbt.filepath()).await.unwrap(), - mbt, - ) + let mut conn = SqliteConnection::connect(mbt.filepath()).await.unwrap(); + attach_hash_fn(&mut conn).await.unwrap(); + (conn, mbt) } #[actix_rt::test] @@ -745,7 +756,7 @@ mod tests { async fn validate_invalid_file() { let (mut conn, mbt) = open("../tests/fixtures/files/invalid/invalid_zoomed_world_cities.mbtiles").await; - let result = mbt.check_agg_tile_hashes(&mut conn).await; + let result = mbt.check_agg_tiles_hashes(&mut conn).await; assert!(matches!(result, Err(MbtError::AggHashMismatch(..)))); } } diff --git a/martin-mbtiles/src/tile_copier.rs b/martin-mbtiles/src/tile_copier.rs index e556b79ac..6827f410f 100644 --- a/martin-mbtiles/src/tile_copier.rs +++ b/martin-mbtiles/src/tile_copier.rs @@ -3,13 +3,14 @@ use std::path::PathBuf; #[cfg(feature = "cli")] use clap::{builder::ValueParser, error::ErrorKind, Args, ValueEnum}; +use sqlite_hashes::rusqlite; use sqlite_hashes::rusqlite::params_from_iter; use sqlx::sqlite::SqliteConnectOptions; use sqlx::{query, Connection, Row, SqliteConnection}; use crate::errors::MbtResult; -use crate::mbtiles::MbtType; use crate::mbtiles::MbtType::{Flat, FlatWithHash, Normalized}; +use crate::mbtiles::{attach_hash_fn, MbtType}; use crate::{MbtError, Mbtiles}; #[derive(PartialEq, Eq, Default, Debug, Clone)] @@ -46,7 +47,7 @@ pub struct TileCopierOptions { /// Compare source file with this file, and only copy non-identical tiles to destination #[cfg_attr(feature = "cli", arg(long))] diff_with_file: Option, - /// Skip generating a global hash for mbtiles validation. By default, if dst_type is flat-with-hash or normalized, generate a global hash and store in the metadata table + /// Skip generating a global hash for mbtiles validation. By default, `mbtiles` will compute `agg_tiles_hash` metadata value. #[cfg_attr(feature = "cli", arg(long))] skip_agg_tiles_hash: bool, } @@ -147,6 +148,10 @@ impl TileCopierOptions { self.skip_agg_tiles_hash = skip_global_hash; self } + + pub async fn run(self) -> MbtResult { + TileCopier::new(self)?.run().await + } } impl TileCopier { @@ -168,6 +173,8 @@ impl TileCopier { ) .await?; + attach_hash_fn(&mut conn).await?; + let is_empty = query!("SELECT 1 as has_rows FROM sqlite_schema LIMIT 1") .fetch_optional(&mut conn) .await? @@ -175,31 +182,27 @@ impl TileCopier { let dst_type = if is_empty { let dst_type = self.options.dst_type.unwrap_or(src_type); - self.create_new_mbtiles(&mut conn, dst_type, src_type) + self.create_new_mbtiles(&mut conn, src_type, dst_type) .await?; dst_type } else if self.options.diff_with_file.is_some() { return Err(MbtError::NonEmptyTargetFile(self.options.dst_file)); } else { - open_and_detect_type(&self.dst_mbtiles).await? + let dst_type = self.dst_mbtiles.detect_type(&mut conn).await?; + attach_source_db(&mut conn, self.src_mbtiles.filepath()).await?; + dst_type }; - let rusqlite_conn = self.dst_mbtiles.open_with_hashes(false)?; - rusqlite_conn.execute( - "ATTACH DATABASE ? AS sourceDb", - [self.src_mbtiles.filepath()], - )?; - let (on_dupl, sql_cond) = self.get_on_duplicate_sql(dst_type); let (select_from, query_args) = { let select_from = if let Some(diff_file) = &self.options.diff_with_file { let diff_with_mbtiles = Mbtiles::new(diff_file)?; let diff_type = open_and_detect_type(&diff_with_mbtiles).await?; - - rusqlite_conn - .execute("ATTACH DATABASE ? AS newDb", [diff_with_mbtiles.filepath()])?; - + let path = diff_with_mbtiles.filepath(); + query!("ATTACH DATABASE ? AS newDb", path) + .execute(&mut conn) + .await?; Self::get_select_from_with_diff(dst_type, diff_type) } else { Self::get_select_from(dst_type, src_type).to_string() @@ -210,6 +213,8 @@ impl TileCopier { (format!("{select_from} {options_sql}"), query_args) }; + let handle = conn.lock_handle().await?.as_raw_handle().as_ptr(); + let rusqlite_conn = unsafe { rusqlite::Connection::from_handle(handle) }?; match dst_type { Flat => rusqlite_conn.execute( &format!("INSERT {on_dupl} INTO tiles {select_from} {sql_cond}"), @@ -237,8 +242,7 @@ impl TileCopier { } }; - if !self.options.skip_agg_tiles_hash && (dst_type == FlatWithHash || dst_type == Normalized) - { + if !self.options.skip_agg_tiles_hash { self.dst_mbtiles.update_agg_tiles_hash(&mut conn).await?; } @@ -248,20 +252,17 @@ impl TileCopier { async fn create_new_mbtiles( &self, conn: &mut SqliteConnection, - dst_type: MbtType, - src_type: MbtType, + src: MbtType, + dst: MbtType, ) -> MbtResult<()> { - let path = self.src_mbtiles.filepath(); - query!("ATTACH DATABASE ? AS sourceDb", path) - .execute(&mut *conn) - .await?; - query!("PRAGMA page_size = 512").execute(&mut *conn).await?; query!("VACUUM").execute(&mut *conn).await?; - if dst_type == src_type { + attach_source_db(&mut *conn, self.src_mbtiles.filepath()).await?; + + if src == dst { // DB objects must be created in a specific order: tables, views, triggers, indexes. - for row in query( + let sql_objects = query( "SELECT sql FROM sourceDb.sqlite_schema WHERE tbl_name IN ('metadata', 'tiles', 'map', 'images', 'tiles_with_hash') @@ -274,19 +275,20 @@ impl TileCopier { ELSE 5 END", ) .fetch_all(&mut *conn) - .await? - { + .await?; + + for row in sql_objects { query(row.get(0)).execute(&mut *conn).await?; } } else { - match dst_type { + match dst { Flat => self.create_flat_tables(&mut *conn).await?, FlatWithHash => self.create_flat_with_hash_tables(&mut *conn).await?, Normalized => self.create_normalized_tables(&mut *conn).await?, }; }; - if dst_type == Normalized { + if dst == Normalized { query( "CREATE VIEW tiles_with_hash AS SELECT @@ -373,7 +375,7 @@ impl TileCopier { }; format!( - "AND NOT EXISTS ( + "AND NOT EXISTS ( SELECT 1 FROM {main_table} WHERE @@ -382,7 +384,7 @@ impl TileCopier { AND {main_table}.tile_row = sourceDb.{main_table}.tile_row AND {main_table}.{tile_identifier} != sourceDb.{main_table}.{tile_identifier} )" - ) + ) }), } } @@ -457,6 +459,13 @@ impl TileCopier { } } +async fn attach_source_db(conn: &mut SqliteConnection, path: &str) -> MbtResult<()> { + query!("ATTACH DATABASE ? AS sourceDb", path) + .execute(&mut *conn) + .await?; + Ok(()) +} + async fn open_and_detect_type(mbtiles: &Mbtiles) -> MbtResult { let opt = SqliteConnectOptions::new() .read_only(true) @@ -472,8 +481,11 @@ pub async fn apply_mbtiles_diff(src_file: PathBuf, diff_file: PathBuf) -> MbtRes let src_type = open_and_detect_type(&src_mbtiles).await?; let diff_type = open_and_detect_type(&diff_mbtiles).await?; - let rusqlite_conn = src_mbtiles.open_with_hashes(false)?; - rusqlite_conn.execute("ATTACH DATABASE ? AS diffDb", [diff_mbtiles.filepath()])?; + let mut conn = src_mbtiles.open_with_hashes(false).await?; + let path = diff_mbtiles.filepath(); + query!("ATTACH DATABASE ? AS diffDb", path) + .execute(&mut conn) + .await?; let select_from = if src_type == Flat { "SELECT zoom_level, tile_column, tile_row, tile_data FROM diffDb.tiles" @@ -499,32 +511,43 @@ pub async fn apply_mbtiles_diff(src_file: PathBuf, diff_file: PathBuf) -> MbtRes }; for statement in insert_sql { - rusqlite_conn.execute(&format!("{statement} WHERE tile_data NOTNULL"), ())?; + query(&format!("{statement} WHERE tile_data NOTNULL")) + .execute(&mut conn) + .await?; } - rusqlite_conn.execute( - &format!( - "DELETE FROM {main_table} + query(&format!( + "DELETE FROM {main_table} WHERE (zoom_level, tile_column, tile_row) IN ( SELECT zoom_level, tile_column, tile_row FROM ({select_from} WHERE tile_data ISNULL) )" - ), - (), - )?; + )) + .execute(&mut conn) + .await?; Ok(()) } -pub async fn copy_mbtiles_file(opts: TileCopierOptions) -> MbtResult { - TileCopier::new(opts)?.run().await -} - #[cfg(test)] mod tests { use sqlx::{Decode, Sqlite, SqliteConnection, Type}; use super::*; + async fn attach_other_db(conn: &mut SqliteConnection, path: &str) -> MbtResult<()> { + query!("ATTACH DATABASE ? AS otherDb", path) + .execute(&mut *conn) + .await?; + Ok(()) + } + + async fn attach_src_db(conn: &mut SqliteConnection, path: &str) -> MbtResult<()> { + query!("ATTACH DATABASE ? AS srcDb", path) + .execute(&mut *conn) + .await?; + Ok(()) + } + async fn get_one(conn: &mut SqliteConnection, sql: &str) -> T where for<'r> T: Decode<'r, Sqlite> + Type, @@ -537,37 +560,34 @@ mod tests { dst_filepath: PathBuf, dst_type: Option, expected_dst_type: MbtType, - ) { - let mut dst_conn = copy_mbtiles_file( - TileCopierOptions::new(src_filepath.clone(), dst_filepath.clone()).dst_type(dst_type), - ) - .await - .unwrap(); + ) -> MbtResult<()> { + let mut dst_conn = TileCopierOptions::new(src_filepath.clone(), dst_filepath.clone()) + .dst_type(dst_type) + .run() + .await?; - query("ATTACH DATABASE ? AS srcDb") - .bind(src_filepath.clone().to_str().unwrap()) - .execute(&mut dst_conn) - .await - .unwrap(); + attach_src_db(&mut dst_conn, src_filepath.to_str().unwrap()).await?; assert_eq!( - open_and_detect_type(&Mbtiles::new(dst_filepath).unwrap()) - .await - .unwrap(), + open_and_detect_type(&Mbtiles::new(dst_filepath)?).await?, expected_dst_type ); assert!( query("SELECT * FROM srcDb.tiles EXCEPT SELECT * FROM tiles") .fetch_optional(&mut dst_conn) - .await - .unwrap() + .await? .is_none() - ) + ); + + Ok(()) } - async fn verify_copy_with_zoom_filter(opts: TileCopierOptions, expected_zoom_levels: u8) { - let mut dst_conn = copy_mbtiles_file(opts).await.unwrap(); + async fn verify_copy_with_zoom_filter( + opts: TileCopierOptions, + expected_zoom_levels: u8, + ) -> MbtResult<()> { + let mut dst_conn = opts.run().await?; assert_eq!( get_one::( @@ -577,104 +597,106 @@ mod tests { .await, expected_zoom_levels ); + + Ok(()) } #[actix_rt::test] - async fn copy_flat_tables() { + async fn copy_flat_tables() -> MbtResult<()> { let src = PathBuf::from("../tests/fixtures/files/world_cities.mbtiles"); let dst = PathBuf::from("file:copy_flat_tables_mem_db?mode=memory&cache=shared"); - verify_copy_all(src, dst, None, Flat).await; + verify_copy_all(src, dst, None, Flat).await } #[actix_rt::test] - async fn copy_flat_from_flat_with_hash_tables() { + async fn copy_flat_from_flat_with_hash_tables() -> MbtResult<()> { let src = PathBuf::from("../tests/fixtures/files/zoomed_world_cities.mbtiles"); let dst = PathBuf::from( "file:copy_flat_from_flat_with_hash_tables_mem_db?mode=memory&cache=shared", ); - verify_copy_all(src, dst, Some(Flat), Flat).await; + verify_copy_all(src, dst, Some(Flat), Flat).await } #[actix_rt::test] - async fn copy_flat_from_normalized_tables() { + async fn copy_flat_from_normalized_tables() -> MbtResult<()> { let src = PathBuf::from("../tests/fixtures/files/geography-class-png.mbtiles"); let dst = PathBuf::from("file:copy_flat_from_normalized_tables_mem_db?mode=memory&cache=shared"); - verify_copy_all(src, dst, Some(Flat), Flat).await; + verify_copy_all(src, dst, Some(Flat), Flat).await } #[actix_rt::test] - async fn copy_flat_with_hash_tables() { + async fn copy_flat_with_hash_tables() -> MbtResult<()> { let src = PathBuf::from("../tests/fixtures/files/zoomed_world_cities.mbtiles"); let dst = PathBuf::from("file:copy_flat_with_hash_tables_mem_db?mode=memory&cache=shared"); - verify_copy_all(src, dst, None, FlatWithHash).await; + verify_copy_all(src, dst, None, FlatWithHash).await } #[actix_rt::test] - async fn copy_flat_with_hash_from_flat_tables() { + async fn copy_flat_with_hash_from_flat_tables() -> MbtResult<()> { let src = PathBuf::from("../tests/fixtures/files/world_cities.mbtiles"); let dst = PathBuf::from( "file:copy_flat_with_hash_from_flat_tables_mem_db?mode=memory&cache=shared", ); - verify_copy_all(src, dst, Some(FlatWithHash), FlatWithHash).await; + verify_copy_all(src, dst, Some(FlatWithHash), FlatWithHash).await } #[actix_rt::test] - async fn copy_flat_with_hash_from_normalized_tables() { + async fn copy_flat_with_hash_from_normalized_tables() -> MbtResult<()> { let src = PathBuf::from("../tests/fixtures/files/geography-class-png.mbtiles"); let dst = PathBuf::from( "file:copy_flat_with_hash_from_normalized_tables_mem_db?mode=memory&cache=shared", ); - verify_copy_all(src, dst, Some(FlatWithHash), FlatWithHash).await; + verify_copy_all(src, dst, Some(FlatWithHash), FlatWithHash).await } #[actix_rt::test] - async fn copy_normalized_tables() { + async fn copy_normalized_tables() -> MbtResult<()> { let src = PathBuf::from("../tests/fixtures/files/geography-class-png.mbtiles"); let dst = PathBuf::from("file:copy_normalized_tables_mem_db?mode=memory&cache=shared"); - verify_copy_all(src, dst, None, Normalized).await; + verify_copy_all(src, dst, None, Normalized).await } #[actix_rt::test] - async fn copy_normalized_from_flat_tables() { + async fn copy_normalized_from_flat_tables() -> MbtResult<()> { let src = PathBuf::from("../tests/fixtures/files/world_cities.mbtiles"); let dst = PathBuf::from("file:copy_normalized_from_flat_tables_mem_db?mode=memory&cache=shared"); - verify_copy_all(src, dst, Some(Normalized), Normalized).await; + verify_copy_all(src, dst, Some(Normalized), Normalized).await } #[actix_rt::test] - async fn copy_normalized_from_flat_with_hash_tables() { + async fn copy_normalized_from_flat_with_hash_tables() -> MbtResult<()> { let src = PathBuf::from("../tests/fixtures/files/zoomed_world_cities.mbtiles"); let dst = PathBuf::from( "file:copy_normalized_from_flat_with_hash_tables_mem_db?mode=memory&cache=shared", ); - verify_copy_all(src, dst, Some(Normalized), Normalized).await; + verify_copy_all(src, dst, Some(Normalized), Normalized).await } #[actix_rt::test] - async fn copy_with_min_max_zoom() { + async fn copy_with_min_max_zoom() -> MbtResult<()> { let src = PathBuf::from("../tests/fixtures/files/world_cities.mbtiles"); let dst = PathBuf::from("file:copy_with_min_max_zoom_mem_db?mode=memory&cache=shared"); let opt = TileCopierOptions::new(src, dst) .min_zoom(Some(2)) .max_zoom(Some(4)); - verify_copy_with_zoom_filter(opt, 3).await; + verify_copy_with_zoom_filter(opt, 3).await } #[actix_rt::test] - async fn copy_with_zoom_levels() { + async fn copy_with_zoom_levels() -> MbtResult<()> { let src = PathBuf::from("../tests/fixtures/files/world_cities.mbtiles"); let dst = PathBuf::from("file:copy_with_zoom_levels_mem_db?mode=memory&cache=shared"); let opt = TileCopierOptions::new(src, dst) .min_zoom(Some(2)) .max_zoom(Some(4)) .zoom_levels(vec![1, 6]); - verify_copy_with_zoom_filter(opt, 2).await; + verify_copy_with_zoom_filter(opt, 2).await } #[actix_rt::test] - async fn copy_with_diff_with_file() { + async fn copy_with_diff_with_file() -> MbtResult<()> { let src = PathBuf::from("../tests/fixtures/files/geography-class-jpg.mbtiles"); let dst = PathBuf::from("file:copy_with_diff_with_file_mem_db?mode=memory&cache=shared"); @@ -684,12 +706,11 @@ mod tests { let copy_opts = TileCopierOptions::new(src.clone(), dst.clone()).diff_with_file(diff_file.clone()); - let mut dst_conn = copy_mbtiles_file(copy_opts).await.unwrap(); + let mut dst_conn = copy_opts.run().await?; assert!(query("SELECT 1 FROM sqlite_schema WHERE name = 'tiles';") .fetch_optional(&mut dst_conn) - .await - .unwrap() + .await? .is_some()); assert_eq!( @@ -717,10 +738,12 @@ mod tests { ) .await .is_none()); + + Ok(()) } #[actix_rt::test] - async fn ignore_dst_type_when_copy_to_existing() { + async fn ignore_dst_type_when_copy_to_existing() -> MbtResult<()> { let src_file = PathBuf::from("../tests/fixtures/files/world_cities_modified.mbtiles"); // Copy the dst file to an in-memory DB @@ -729,11 +752,11 @@ mod tests { "file:ignore_dst_type_when_copy_to_existing_mem_db?mode=memory&cache=shared", ); - let _dst_conn = copy_mbtiles_file(TileCopierOptions::new(dst_file.clone(), dst.clone())) - .await - .unwrap(); + let _dst_conn = TileCopierOptions::new(dst_file.clone(), dst.clone()) + .run() + .await?; - verify_copy_all(src_file, dst, Some(Normalized), Flat).await; + verify_copy_all(src_file, dst, Some(Normalized), Flat).await } #[actix_rt::test] @@ -745,13 +768,13 @@ mod tests { TileCopierOptions::new(src.clone(), dst.clone()).on_duplicate(CopyDuplicateMode::Abort); assert!(matches!( - copy_mbtiles_file(copy_opts).await.unwrap_err(), + copy_opts.run().await.unwrap_err(), MbtError::RusqliteError(..) )); } #[actix_rt::test] - async fn copy_to_existing_override_mode() { + async fn copy_to_existing_override_mode() -> MbtResult<()> { let src_file = PathBuf::from("../tests/fixtures/files/world_cities_modified.mbtiles"); // Copy the dst file to an in-memory DB @@ -759,32 +782,28 @@ mod tests { let dst = PathBuf::from("file:copy_to_existing_override_mode_mem_db?mode=memory&cache=shared"); - let _dst_conn = copy_mbtiles_file(TileCopierOptions::new(dst_file.clone(), dst.clone())) - .await - .unwrap(); + let _dst_conn = TileCopierOptions::new(dst_file.clone(), dst.clone()) + .run() + .await?; - let mut dst_conn = copy_mbtiles_file(TileCopierOptions::new(src_file.clone(), dst.clone())) - .await - .unwrap(); + let mut dst_conn = TileCopierOptions::new(src_file.clone(), dst.clone()) + .run() + .await?; // Verify the tiles in the destination file is a superset of the tiles in the source file - query("ATTACH DATABASE ? AS otherDb") - .bind(src_file.clone().to_str().unwrap()) - .execute(&mut dst_conn) - .await - .unwrap(); - + attach_other_db(&mut dst_conn, src_file.to_str().unwrap()).await?; assert!( query("SELECT * FROM otherDb.tiles EXCEPT SELECT * FROM tiles;") .fetch_optional(&mut dst_conn) - .await - .unwrap() + .await? .is_none() ); + + Ok(()) } #[actix_rt::test] - async fn copy_to_existing_ignore_mode() { + async fn copy_to_existing_ignore_mode() -> MbtResult<()> { let src_file = PathBuf::from("../tests/fixtures/files/world_cities_modified.mbtiles"); // Copy the dst file to an in-memory DB @@ -792,28 +811,23 @@ mod tests { let dst = PathBuf::from("file:copy_to_existing_ignore_mode_mem_db?mode=memory&cache=shared"); - let _dst_conn = copy_mbtiles_file(TileCopierOptions::new(dst_file.clone(), dst.clone())) - .await - .unwrap(); + let _dst_conn = TileCopierOptions::new(dst_file.clone(), dst.clone()) + .run() + .await?; - let mut dst_conn = copy_mbtiles_file( - TileCopierOptions::new(src_file.clone(), dst.clone()) - .on_duplicate(CopyDuplicateMode::Ignore), - ) - .await - .unwrap(); + let mut dst_conn = TileCopierOptions::new(src_file.clone(), dst.clone()) + .on_duplicate(CopyDuplicateMode::Ignore) + .run() + .await?; // Verify the tiles in the destination file are the same as those in the source file except for those with duplicate (zoom_level, tile_column, tile_row) - query("ATTACH DATABASE ? AS srcDb") - .bind(src_file.clone().to_str().unwrap()) - .execute(&mut dst_conn) - .await - .unwrap(); - query("ATTACH DATABASE ? AS originalDb") - .bind(dst_file.clone().to_str().unwrap()) + attach_src_db(&mut dst_conn, src_file.to_str().unwrap()).await?; + + let path = dst_file.to_str().unwrap(); + query!("ATTACH DATABASE ? AS originalDb", path) .execute(&mut dst_conn) - .await - .unwrap(); + .await?; + // Create a temporary table with all the tiles in the original database and // all the tiles in the source database except for those that conflict with tiles in the original database query("CREATE TEMP TABLE expected_tiles AS @@ -826,7 +840,7 @@ mod tests { ON t1.zoom_level = t2.zoom_level AND t1.tile_column = t2.tile_column AND t1.tile_row = t2.tile_row") .execute(&mut dst_conn) .await - .unwrap(); + ?; // Ensure all entries in expected_tiles are in tiles and vice versa assert!(query( @@ -835,68 +849,65 @@ mod tests { SELECT * FROM tiles EXCEPT SELECT * FROM expected_tiles" ) .fetch_optional(&mut dst_conn) - .await - .unwrap() + .await? .is_none()); + + Ok(()) } #[actix_rt::test] - async fn apply_flat_diff_file() { + async fn apply_flat_diff_file() -> MbtResult<()> { // Copy the src file to an in-memory DB let src_file = PathBuf::from("../tests/fixtures/files/world_cities.mbtiles"); let src = PathBuf::from("file:apply_flat_diff_file_mem_db?mode=memory&cache=shared"); - let mut src_conn = copy_mbtiles_file(TileCopierOptions::new(src_file.clone(), src.clone())) - .await - .unwrap(); + let mut src_conn = TileCopierOptions::new(src_file.clone(), src.clone()) + .run() + .await?; // Apply diff to the src data in in-memory DB let diff_file = PathBuf::from("../tests/fixtures/files/world_cities_diff.mbtiles"); - apply_mbtiles_diff(src, diff_file).await.unwrap(); + apply_mbtiles_diff(src, diff_file).await?; // Verify the data is the same as the file the diff was generated from let path = "../tests/fixtures/files/world_cities_modified.mbtiles"; - query!("ATTACH DATABASE ? AS otherDb", path) - .execute(&mut src_conn) - .await - .unwrap(); + attach_other_db(&mut src_conn, path).await?; assert!( query("SELECT * FROM tiles EXCEPT SELECT * FROM otherDb.tiles;") .fetch_optional(&mut src_conn) - .await - .unwrap() + .await? .is_none() ); + + Ok(()) } #[actix_rt::test] - async fn apply_normalized_diff_file() { + async fn apply_normalized_diff_file() -> MbtResult<()> { // Copy the src file to an in-memory DB let src_file = PathBuf::from("../tests/fixtures/files/geography-class-jpg.mbtiles"); let src = PathBuf::from("file:apply_normalized_diff_file_mem_db?mode=memory&cache=shared"); - let mut src_conn = copy_mbtiles_file(TileCopierOptions::new(src_file.clone(), src.clone())) - .await - .unwrap(); + let mut src_conn = TileCopierOptions::new(src_file.clone(), src.clone()) + .run() + .await?; // Apply diff to the src data in in-memory DB let diff_file = PathBuf::from("../tests/fixtures/files/geography-class-jpg-diff.mbtiles"); - apply_mbtiles_diff(src, diff_file).await.unwrap(); + apply_mbtiles_diff(src, diff_file).await?; // Verify the data is the same as the file the diff was generated from let path = "../tests/fixtures/files/geography-class-jpg-modified.mbtiles"; - query!("ATTACH DATABASE ? AS otherDb", path) - .execute(&mut src_conn) - .await - .unwrap(); + attach_other_db(&mut src_conn, path).await?; assert!( query("SELECT * FROM tiles EXCEPT SELECT * FROM otherDb.tiles;") .fetch_optional(&mut src_conn) - .await - .unwrap() + .await? .is_none() ); + + Ok(()) } } From 1a386b7071f0e5d4f7265ac12f6a7aff0706aaa0 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Thu, 28 Sep 2023 16:57:50 -0400 Subject: [PATCH 002/108] Fix unit tests to use blobs only --- Cargo.lock | 24 +++++++++--------- martin-mbtiles/src/mbtiles.rs | 5 ++-- .../fixtures/files/world_cities_diff.mbtiles | Bin 3584 -> 3584 bytes .../files/world_cities_modified.mbtiles | Bin 49152 -> 49152 bytes 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 097c9d88e..a09f224f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -298,9 +298,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c" +checksum = "bff2cf94a3dbe2d57cbd56485e1bd7436455058034d6c2d47be51d4e5e4bc6ab" dependencies = [ "anstyle", "anstyle-parse", @@ -312,15 +312,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b84bf0a05bbb2a83e5eb6fa36bb6e87baa08193c35ff52bbf6b38d8af2890e46" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" [[package]] name = "anstyle-parse" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" +checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" dependencies = [ "utf8parse", ] @@ -336,9 +336,9 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "2.1.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd" +checksum = "0238ca56c96dfa37bdf7c373c8886dd591322500aceeeccdb2216fe06dc2f796" dependencies = [ "anstyle", "windows-sys", @@ -614,9 +614,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.5" +version = "4.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824956d0dca8334758a5b7f7e50518d66ea319330cbceedcf76905c2f6ab30e3" +checksum = "d04704f56c2cde07f43e8e2c154b43f216dc5c92fc98ada720177362f953b956" dependencies = [ "clap_builder", "clap_derive", @@ -624,9 +624,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.5" +version = "4.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "122ec64120a49b4563ccaedcbea7818d069ed8e9aa6d829b82d8a4128936b2ab" +checksum = "0e231faeaca65ebd1ea3c737966bf858971cd38c3849107aa3ea7de90a804e45" dependencies = [ "anstream", "anstyle", diff --git a/martin-mbtiles/src/mbtiles.rs b/martin-mbtiles/src/mbtiles.rs index b37cbd8df..add902fe5 100644 --- a/martin-mbtiles/src/mbtiles.rs +++ b/martin-mbtiles/src/mbtiles.rs @@ -564,7 +564,8 @@ where let query = query( // The md5_concat func will return NULL if there are no rows in the tiles table. // For our use case, we will treat it as an empty string, and hash that. - // Note that in some weird rare cases, a column with blob type may be stored as an integer value + // `tile_data` values must be stored as a blob per MBTiles spec + // `md5` functions will fail if the value is not text/blob/null "SELECT hex( coalesce( @@ -572,7 +573,7 @@ where cast(zoom_level AS text), cast(tile_column AS text), cast(tile_row AS text), - cast(tile_data as blob) + tile_data ), md5('') ) diff --git a/tests/fixtures/files/world_cities_diff.mbtiles b/tests/fixtures/files/world_cities_diff.mbtiles index 275f3316553c4b7e350951419655f2b24e7cc710..bcb123d983543e4ada9c40d8f0c6b4fadb1096cc 100644 GIT binary patch delta 78 zcmZpWX^@#9&B!@X#+j9qK`-2EW6E4+UdB%h%#1e~7=JR}WPGw&kmWR^0w)tICnu+f hL4~0K4>KzxBclil3yVR8i4g+>13M6J{>jSD0RY%*5DowU delta 65 zcmZpWX^@#9&B#7c#+j9!K`-sq#+13tybKHs%#6<&7=JQ;Vtl??kmClUFaswuDjSD0RRaf4YdFO diff --git a/tests/fixtures/files/world_cities_modified.mbtiles b/tests/fixtures/files/world_cities_modified.mbtiles index 82fa5564d7e1ccb0aa9ad9e33a9489fe6ada9b6f..e6d104bf730a9af5db838195f345bae9e6b817e2 100644 GIT binary patch delta 86 zcmV-c0IC0gfCGSl1CSd56OkN41rq=+S4y#Dt4{$=vk*|Z1p$b$frJ7J2?YfS2?-Q1 scrh@uSy-({0j-kQ0KT*b+m7hT`>%zvA)&7h@n*{=PvNHB;KBd~h#Lmjf$_WIUBO}(j hGj5p7zCeX>!^XxIE*{QCS5`(KU;%>7dsexK0sy>V7Lot} From f52cd3c611c3435fad5cb6472ad6c48a39f5c517 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Thu, 28 Sep 2023 18:19:05 -0400 Subject: [PATCH 003/108] Minor mbtiles doc cleanups --- docs/src/tools.md | 4 +++- martin-mbtiles/src/bin/main.rs | 23 +++++++++++------------ martin-mbtiles/src/lib.rs | 2 +- martin-mbtiles/src/mbtiles.rs | 1 - 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/src/tools.md b/docs/src/tools.md index a2e35b5e0..bbb63082c 100644 --- a/docs/src/tools.md +++ b/docs/src/tools.md @@ -74,7 +74,9 @@ The original [MBTiles specification](https://github.com/mapbox/mbtiles-spec#read A typical Normalized schema generated by tools like [tilelive-copy](https://github.com/mapbox/TileLive#bintilelive-copy) use MD5 hash in the `tile_id` column. The Martin's `mbtiles` tool can use this hash to verify the content of each tile. We also define a new `flat-with-hash` schema that stores the hash and tile data in the same table. This schema is more efficient than the `normalized` schema when data has no duplicate tiles (see below). Per tile validation is not available for `flat` schema. -Per-tile validation will catch individual invalid tiles, but it will not detect overall datastore corruption (e.g. missing tiles or tiles that shouldn't exist, or tiles with incorrect z/x/y values). For that, Martin `mbtiles` tool defines a new metadata value called `agg_tiles_hash`. The value is computed by hashing `cast(zoom_level AS text), cast(tile_column AS text), cast(tile_row AS text), cast(tile_data as blob)` combined for all rows in the `tiles` table/view, ordered by z,x,y. In case there are no rows or all are NULL, the hash value of an empty string is used. +Per-tile validation will catch individual invalid tiles, but it will not detect overall datastore corruption (e.g. missing tiles or tiles that shouldn't exist, or tiles with incorrect z/x/y values). +For that, Martin `mbtiles` tool defines a new metadata value called `agg_tiles_hash`. The value is computed by hashing `cast(zoom_level AS text), cast(tile_column AS text), cast(tile_row AS text), tile_data` combined for all rows in the `tiles` table/view, ordered by z,x,y. +In case there are no rows or all are NULL, the hash value of an empty string is used. Note that SQLite allows any value type to be stored as in any column, so if `tile_data` accidentally contains non-blob/text/null value, validation will fail. The `mbtiles` tool will compute `agg_tiles_hash` value when copying or validating mbtiles files. diff --git a/martin-mbtiles/src/bin/main.rs b/martin-mbtiles/src/bin/main.rs index 39579703b..5a7b0f542 100644 --- a/martin-mbtiles/src/bin/main.rs +++ b/martin-mbtiles/src/bin/main.rs @@ -1,8 +1,9 @@ use std::path::{Path, PathBuf}; -use anyhow::Result; use clap::{Parser, Subcommand}; -use martin_mbtiles::{apply_mbtiles_diff, IntegrityCheckType, Mbtiles, TileCopierOptions}; +use martin_mbtiles::{ + apply_mbtiles_diff, IntegrityCheckType, MbtResult, Mbtiles, TileCopierOptions, +}; use sqlx::sqlite::SqliteConnectOptions; use sqlx::{Connection, SqliteConnection}; @@ -72,7 +73,7 @@ enum Commands { } #[tokio::main] -async fn main() -> Result<()> { +async fn main() -> anyhow::Result<()> { let args = Args::parse(); match args.command { @@ -106,7 +107,7 @@ async fn main() -> Result<()> { Ok(()) } -async fn meta_print_all(file: &Path) -> Result<()> { +async fn meta_print_all(file: &Path) -> anyhow::Result<()> { let mbt = Mbtiles::new(file)?; let opt = SqliteConnectOptions::new().filename(file).read_only(true); let mut conn = SqliteConnection::connect_with(&opt).await?; @@ -115,7 +116,7 @@ async fn meta_print_all(file: &Path) -> Result<()> { Ok(()) } -async fn meta_get_value(file: &Path, key: &str) -> Result<()> { +async fn meta_get_value(file: &Path, key: &str) -> MbtResult<()> { let mbt = Mbtiles::new(file)?; let opt = SqliteConnectOptions::new().filename(file).read_only(true); let mut conn = SqliteConnection::connect_with(&opt).await?; @@ -125,30 +126,28 @@ async fn meta_get_value(file: &Path, key: &str) -> Result<()> { Ok(()) } -async fn meta_set_value(file: &Path, key: &str, value: Option) -> Result<()> { +async fn meta_set_value(file: &Path, key: &str, value: Option) -> MbtResult<()> { let mbt = Mbtiles::new(file)?; let opt = SqliteConnectOptions::new().filename(file); let mut conn = SqliteConnection::connect_with(&opt).await?; - mbt.set_metadata_value(&mut conn, key, value).await?; - Ok(()) + mbt.set_metadata_value(&mut conn, key, value).await } async fn validate_mbtiles( file: &Path, check_type: IntegrityCheckType, update_agg_tiles_hash: bool, -) -> Result<()> { +) -> MbtResult<()> { let mbt = Mbtiles::new(file)?; let opt = SqliteConnectOptions::new().filename(file).read_only(true); let mut conn = SqliteConnection::connect_with(&opt).await?; mbt.check_integrity(&mut conn, check_type).await?; mbt.check_each_tile_hash(&mut conn).await?; if update_agg_tiles_hash { - mbt.update_agg_tiles_hash(&mut conn).await?; + mbt.update_agg_tiles_hash(&mut conn).await } else { - mbt.check_agg_tiles_hashes(&mut conn).await?; + mbt.check_agg_tiles_hashes(&mut conn).await } - Ok(()) } #[cfg(test)] diff --git a/martin-mbtiles/src/lib.rs b/martin-mbtiles/src/lib.rs index 4d90e4231..16a9747dc 100644 --- a/martin-mbtiles/src/lib.rs +++ b/martin-mbtiles/src/lib.rs @@ -6,7 +6,7 @@ mod mbtiles_pool; mod mbtiles_queries; mod tile_copier; -pub use errors::MbtError; +pub use errors::{MbtError, MbtResult}; pub use mbtiles::{IntegrityCheckType, Mbtiles, Metadata}; pub use mbtiles_pool::MbtilesPool; pub use tile_copier::{apply_mbtiles_diff, CopyDuplicateMode, TileCopierOptions}; diff --git a/martin-mbtiles/src/mbtiles.rs b/martin-mbtiles/src/mbtiles.rs index add902fe5..6a1c37242 100644 --- a/martin-mbtiles/src/mbtiles.rs +++ b/martin-mbtiles/src/mbtiles.rs @@ -470,7 +470,6 @@ impl Mbtiles { return Err(AggHashValueNotFound(self.filepath().to_string())); }; - // let conn = self.open_with_hashes(true)?; let computed = calc_agg_tiles_hash(&mut *conn).await?; if stored != computed { let file = self.filepath().to_string(); From 73edd19ef9aeb5d078d6573a50f98de1c5886d7f Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Fri, 29 Sep 2023 13:37:40 -0400 Subject: [PATCH 004/108] Hide pg connection password (#907) the output now looks like this, with the password being shown as `_` instead of a real value. Connecting to Config { user: Some("myname"), password: Some(_), dbname: Some("db"), options: None, application_name: None, ssl_mode: Prefer, host: [Tcp("localhost")], hostaddr: [], port: [5411], connect_timeout: None, tcp_user_timeout: None, keepalives: true, keepalives_idle: 7200s, keepalives_interval: None, keepalives_retries: None, target_session_attrs: Any, channel_binding: Prefer } Fix #902 --- martin/src/pg/pool.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/martin/src/pg/pool.rs b/martin/src/pg/pool.rs index 4ebceb572..15b26b9d9 100755 --- a/martin/src/pg/pool.rs +++ b/martin/src/pg/pool.rs @@ -3,7 +3,7 @@ use log::{info, warn}; use semver::Version; use crate::pg::config::PgConfig; -use crate::pg::tls::{make_connector, parse_conn_str}; +use crate::pg::tls::{make_connector, parse_conn_str, SslModeOverride}; use crate::pg::PgError::{ BadPostgisVersion, PostgisTooOld, PostgresError, PostgresPoolBuildError, PostgresPoolConnError, }; @@ -28,8 +28,12 @@ pub struct PgPool { impl PgPool { pub async fn new(config: &PgConfig) -> Result { let conn_str = config.connection_string.as_ref().unwrap().as_str(); - info!("Connecting to {conn_str}"); let (pg_cfg, ssl_mode) = parse_conn_str(conn_str)?; + if matches!(ssl_mode, SslModeOverride::Unmodified(_)) { + info!("Connecting to {pg_cfg:?}"); + } else { + info!("Connecting to {pg_cfg:?} with ssl_mode={ssl_mode:?}"); + } let id = pg_cfg.get_dbname().map_or_else( || format!("{:?}", pg_cfg.get_hosts()[0]), From 6f08aa9465a700a6925ca3f943842c2dc04a5f9c Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Fri, 29 Sep 2023 14:37:18 -0400 Subject: [PATCH 005/108] Fix mbtiles validation, CI, and logging (#903) --- Cargo.lock | 1 + justfile | 3 +- martin-mbtiles/Cargo.toml | 3 +- martin-mbtiles/src/bin/main.rs | 33 +++++++---- martin-mbtiles/src/mbtiles.rs | 51 ++++++++--------- martin-mbtiles/src/tile_copier.rs | 54 +++++++++--------- tests/config.yaml | 2 +- tests/expected/generated_config.yaml | 38 ++++++------ tests/expected/given_config.yaml | 2 +- tests/expected/mbtiles/copy_diff.txt | 1 + tests/expected/mbtiles/copy_diff2.txt | 1 + tests/expected/mbtiles/validate-bad.txt | 3 + tests/expected/mbtiles/validate-fix.txt | 3 + tests/expected/mbtiles/validate-fix2.txt | 3 + tests/expected/mbtiles/validate-ok.txt | 3 + tests/fixtures/files/bad_hash.mbtiles | Bin 0 -> 14848 bytes .../{invalid => }/invalid-tile-format.mbtiles | Bin .../files/{invalid => }/invalid.mbtiles | Bin .../invalid_zoomed_world_cities.mbtiles | Bin .../geography-class-jpg-diff.mbtiles | Bin .../geography-class-jpg-modified.mbtiles | Bin .../geography-class-jpg.mbtiles | Bin .../geography-class-png-no-bounds.mbtiles | Bin .../geography-class-png.mbtiles | Bin .../fixtures/{files => mbtiles}/json.mbtiles | Bin .../uncompressed_mvt.mbtiles | Bin .../fixtures/{files => mbtiles}/webp.mbtiles | Bin .../{files => mbtiles}/world_cities.mbtiles | Bin .../world_cities_diff.mbtiles | Bin .../world_cities_modified.mbtiles | Bin .../zoomed_world_cities.mbtiles | Bin tests/fixtures/{files => pmtiles}/png.pmtiles | Bin ...stamen_toner__raster_CC-BY+ODbL_z3.pmtiles | Bin .../fixtures/{files => pmtiles}/webp2.pmtiles | Bin tests/mb_server_test.rs | 8 +-- tests/pg_table_source_test.rs | 2 +- tests/pmt_server_test.rs | 4 +- tests/test.sh | 29 +++++++--- 38 files changed, 143 insertions(+), 101 deletions(-) create mode 100644 tests/expected/mbtiles/validate-bad.txt create mode 100644 tests/expected/mbtiles/validate-fix.txt create mode 100644 tests/expected/mbtiles/validate-fix2.txt create mode 100644 tests/expected/mbtiles/validate-ok.txt create mode 100644 tests/fixtures/files/bad_hash.mbtiles rename tests/fixtures/files/{invalid => }/invalid-tile-format.mbtiles (100%) rename tests/fixtures/files/{invalid => }/invalid.mbtiles (100%) rename tests/fixtures/files/{invalid => }/invalid_zoomed_world_cities.mbtiles (100%) rename tests/fixtures/{files => mbtiles}/geography-class-jpg-diff.mbtiles (100%) rename tests/fixtures/{files => mbtiles}/geography-class-jpg-modified.mbtiles (100%) rename tests/fixtures/{files => mbtiles}/geography-class-jpg.mbtiles (100%) rename tests/fixtures/{files => mbtiles}/geography-class-png-no-bounds.mbtiles (100%) rename tests/fixtures/{files => mbtiles}/geography-class-png.mbtiles (100%) rename tests/fixtures/{files => mbtiles}/json.mbtiles (100%) rename tests/fixtures/{files => mbtiles}/uncompressed_mvt.mbtiles (100%) rename tests/fixtures/{files => mbtiles}/webp.mbtiles (100%) rename tests/fixtures/{files => mbtiles}/world_cities.mbtiles (100%) rename tests/fixtures/{files => mbtiles}/world_cities_diff.mbtiles (100%) rename tests/fixtures/{files => mbtiles}/world_cities_modified.mbtiles (100%) rename tests/fixtures/{files => mbtiles}/zoomed_world_cities.mbtiles (100%) rename tests/fixtures/{files => pmtiles}/png.pmtiles (100%) rename tests/fixtures/{files => pmtiles}/stamen_toner__raster_CC-BY+ODbL_z3.pmtiles (100%) rename tests/fixtures/{files => pmtiles}/webp2.pmtiles (100%) diff --git a/Cargo.lock b/Cargo.lock index a09f224f3..0fadd1617 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1799,6 +1799,7 @@ dependencies = [ "actix-rt", "anyhow", "clap", + "env_logger", "futures", "log", "martin-tile-utils", diff --git a/justfile b/justfile index 503a4df79..a8aacc642 100644 --- a/justfile +++ b/justfile @@ -110,6 +110,7 @@ test-int: clean-test install-sqlx # Run integration tests and save its output as the new expected output bless: start clean-test + rm -rf tests/temp cargo test --features bless-tests tests/test.sh rm -rf tests/expected @@ -216,7 +217,7 @@ git-pre-push: stop start # Update sqlite database schema. prepare-sqlite: install-sqlx mkdir -p martin-mbtiles/.sqlx - cd martin-mbtiles && cargo sqlx prepare --database-url sqlite://$PWD/../tests/fixtures/files/world_cities.mbtiles -- --lib --tests + cd martin-mbtiles && cargo sqlx prepare --database-url sqlite://$PWD/../tests/fixtures/mbtiles/world_cities.mbtiles -- --lib --tests # Install SQLX cli if not already installed. [private] diff --git a/martin-mbtiles/Cargo.toml b/martin-mbtiles/Cargo.toml index 3392c25a9..fd746f043 100644 --- a/martin-mbtiles/Cargo.toml +++ b/martin-mbtiles/Cargo.toml @@ -12,7 +12,7 @@ rust-version.workspace = true [features] # TODO: Disable "cli" feature in default builds default = ["cli", "native-tls"] -cli = ["dep:anyhow", "dep:clap", "dep:tokio"] +cli = ["dep:anyhow", "dep:clap", "dep:env_logger", "dep:tokio"] # One of the following two must be used native-tls = ["sqlx/runtime-tokio-native-tls"] rustls = ["sqlx/runtime-tokio-rustls"] @@ -30,6 +30,7 @@ tilejson.workspace = true # Bin dependencies anyhow = { workspace = true, optional = true } clap = { workspace = true, optional = true } +env_logger = { workspace = true, optional = true } serde_yaml.workspace = true sqlite-hashes.workspace = true tokio = { workspace = true, features = ["rt-multi-thread"], optional = true } diff --git a/martin-mbtiles/src/bin/main.rs b/martin-mbtiles/src/bin/main.rs index 5a7b0f542..4df966ce1 100644 --- a/martin-mbtiles/src/bin/main.rs +++ b/martin-mbtiles/src/bin/main.rs @@ -1,11 +1,10 @@ use std::path::{Path, PathBuf}; use clap::{Parser, Subcommand}; +use log::{error, LevelFilter}; use martin_mbtiles::{ apply_mbtiles_diff, IntegrityCheckType, MbtResult, Mbtiles, TileCopierOptions, }; -use sqlx::sqlite::SqliteConnectOptions; -use sqlx::{Connection, SqliteConnection}; #[derive(Parser, PartialEq, Eq, Debug)] #[command( @@ -73,9 +72,23 @@ enum Commands { } #[tokio::main] -async fn main() -> anyhow::Result<()> { - let args = Args::parse(); +async fn main() { + env_logger::builder() + .filter_level(LevelFilter::Info) + .format_indent(None) + .format_module_path(false) + .format_target(false) + .format_timestamp(None) + .init(); + if let Err(err) = main_int().await { + error!("{err}"); + std::process::exit(1); + } +} + +async fn main_int() -> anyhow::Result<()> { + let args = Args::parse(); match args.command { Commands::MetaAll { file } => { meta_print_all(file.as_path()).await?; @@ -109,8 +122,7 @@ async fn main() -> anyhow::Result<()> { async fn meta_print_all(file: &Path) -> anyhow::Result<()> { let mbt = Mbtiles::new(file)?; - let opt = SqliteConnectOptions::new().filename(file).read_only(true); - let mut conn = SqliteConnection::connect_with(&opt).await?; + let mut conn = mbt.open_with_hashes(true).await?; let metadata = mbt.get_metadata(&mut conn).await?; println!("{}", serde_yaml::to_string(&metadata)?); Ok(()) @@ -118,8 +130,7 @@ async fn meta_print_all(file: &Path) -> anyhow::Result<()> { async fn meta_get_value(file: &Path, key: &str) -> MbtResult<()> { let mbt = Mbtiles::new(file)?; - let opt = SqliteConnectOptions::new().filename(file).read_only(true); - let mut conn = SqliteConnection::connect_with(&opt).await?; + let mut conn = mbt.open_with_hashes(true).await?; if let Some(s) = mbt.get_metadata_value(&mut conn, key).await? { println!("{s}"); } @@ -128,8 +139,7 @@ async fn meta_get_value(file: &Path, key: &str) -> MbtResult<()> { async fn meta_set_value(file: &Path, key: &str, value: Option) -> MbtResult<()> { let mbt = Mbtiles::new(file)?; - let opt = SqliteConnectOptions::new().filename(file); - let mut conn = SqliteConnection::connect_with(&opt).await?; + let mut conn = mbt.open_with_hashes(false).await?; mbt.set_metadata_value(&mut conn, key, value).await } @@ -139,8 +149,7 @@ async fn validate_mbtiles( update_agg_tiles_hash: bool, ) -> MbtResult<()> { let mbt = Mbtiles::new(file)?; - let opt = SqliteConnectOptions::new().filename(file).read_only(true); - let mut conn = SqliteConnection::connect_with(&opt).await?; + let mut conn = mbt.open_with_hashes(!update_agg_tiles_hash).await?; mbt.check_integrity(&mut conn, check_type).await?; mbt.check_each_tile_hash(&mut conn).await?; if update_agg_tiles_hash { diff --git a/martin-mbtiles/src/mbtiles.rs b/martin-mbtiles/src/mbtiles.rs index 6a1c37242..a3d350dc9 100644 --- a/martin-mbtiles/src/mbtiles.rs +++ b/martin-mbtiles/src/mbtiles.rs @@ -435,7 +435,9 @@ impl Mbtiles { where for<'e> &'e mut T: SqliteExecutor<'e>, { + let filepath = self.filepath(); if integrity_check == IntegrityCheckType::Off { + info!("Skipping integrity check for {filepath}"); return Ok(()); } @@ -452,13 +454,14 @@ impl Mbtiles { if result.len() > 1 || result.get(0).ok_or(FailedIntegrityCheck( - self.filepath().to_string(), + filepath.to_string(), vec!["SQLite could not perform integrity check".to_string()], ))? != "ok" { return Err(FailedIntegrityCheck(self.filepath().to_string(), result)); } + info!("{integrity_check:?} integrity check passed for {filepath}"); Ok(()) } @@ -466,16 +469,17 @@ impl Mbtiles { where for<'e> &'e mut T: SqliteExecutor<'e>, { + let filepath = self.filepath(); let Some(stored) = self.get_agg_tiles_hash(&mut *conn).await? else { - return Err(AggHashValueNotFound(self.filepath().to_string())); + return Err(AggHashValueNotFound(filepath.to_string())); }; - let computed = calc_agg_tiles_hash(&mut *conn).await?; if stored != computed { - let file = self.filepath().to_string(); + let file = filepath.to_string(); return Err(AggHashMismatch(computed, stored, file)); } + info!("The agg_tiles_hashes={computed} has been verified for {filepath}"); Ok(()) } @@ -486,23 +490,15 @@ impl Mbtiles { { let old_hash = self.get_agg_tiles_hash(&mut *conn).await?; let hash = calc_agg_tiles_hash(&mut *conn).await?; + let path = self.filepath(); if old_hash.as_ref() == Some(&hash) { - info!( - "agg_tiles_hash is already set to the correct value `{hash}` in {}", - self.filepath() - ); + info!("agg_tiles_hash is already set to the correct value `{hash}` in {path}"); Ok(()) } else { if let Some(old_hash) = old_hash { - info!( - "Updating agg_tiles_hash from {old_hash} to {hash} in {}", - self.filepath() - ); + info!("Updating agg_tiles_hash from {old_hash} to {hash} in {path}"); } else { - info!( - "Initializing agg_tiles_hash to {hash} in {}", - self.filepath() - ); + info!("Creating new metadata value agg_tiles_hash = {hash} in {path}"); } self.set_metadata_value(&mut *conn, "agg_tiles_hash", Some(hash)) .await @@ -550,7 +546,10 @@ impl Mbtiles { v.get(0), v.get(1), )) - }) + })?; + + info!("All tile hashes are valid for {}", self.filepath()); + Ok(()) } } @@ -610,7 +609,7 @@ mod tests { #[actix_rt::test] async fn mbtiles_meta() { - let filepath = "../tests/fixtures/files/geography-class-jpg.mbtiles"; + let filepath = "../tests/fixtures/mbtiles/geography-class-jpg.mbtiles"; let mbt = Mbtiles::new(filepath).unwrap(); assert_eq!(mbt.filepath(), filepath); assert_eq!(mbt.filename(), "geography-class-jpg"); @@ -618,7 +617,7 @@ mod tests { #[actix_rt::test] async fn metadata_jpeg() { - let (mut conn, mbt) = open("../tests/fixtures/files/geography-class-jpg.mbtiles").await; + let (mut conn, mbt) = open("../tests/fixtures/mbtiles/geography-class-jpg.mbtiles").await; let metadata = mbt.get_metadata(&mut conn).await.unwrap(); let tj = metadata.tilejson; @@ -635,7 +634,7 @@ mod tests { #[actix_rt::test] async fn metadata_mvt() { - let (mut conn, mbt) = open("../tests/fixtures/files/world_cities.mbtiles").await; + let (mut conn, mbt) = open("../tests/fixtures/mbtiles/world_cities.mbtiles").await; let metadata = mbt.get_metadata(&mut conn).await.unwrap(); let tj = metadata.tilejson; @@ -666,7 +665,7 @@ mod tests { #[actix_rt::test] async fn metadata_get_key() { - let (mut conn, mbt) = open("../tests/fixtures/files/world_cities.mbtiles").await; + let (mut conn, mbt) = open("../tests/fixtures/mbtiles/world_cities.mbtiles").await; let res = mbt.get_metadata_value(&mut conn, "bounds").await.unwrap(); assert_eq!(res.unwrap(), "-123.123590,-37.818085,174.763027,59.352706"); @@ -726,15 +725,15 @@ mod tests { #[actix_rt::test] async fn detect_type() { - let (mut conn, mbt) = open("../tests/fixtures/files/world_cities.mbtiles").await; + let (mut conn, mbt) = open("../tests/fixtures/mbtiles/world_cities.mbtiles").await; let res = mbt.detect_type(&mut conn).await.unwrap(); assert_eq!(res, MbtType::Flat); - let (mut conn, mbt) = open("../tests/fixtures/files/zoomed_world_cities.mbtiles").await; + let (mut conn, mbt) = open("../tests/fixtures/mbtiles/zoomed_world_cities.mbtiles").await; let res = mbt.detect_type(&mut conn).await.unwrap(); assert_eq!(res, MbtType::FlatWithHash); - let (mut conn, mbt) = open("../tests/fixtures/files/geography-class-jpg.mbtiles").await; + let (mut conn, mbt) = open("../tests/fixtures/mbtiles/geography-class-jpg.mbtiles").await; let res = mbt.detect_type(&mut conn).await.unwrap(); assert_eq!(res, MbtType::Normalized); @@ -745,7 +744,7 @@ mod tests { #[actix_rt::test] async fn validate_valid_file() { - let (mut conn, mbt) = open("../tests/fixtures/files/zoomed_world_cities.mbtiles").await; + let (mut conn, mbt) = open("../tests/fixtures/mbtiles/zoomed_world_cities.mbtiles").await; mbt.check_integrity(&mut conn, IntegrityCheckType::Quick) .await @@ -755,7 +754,7 @@ mod tests { #[actix_rt::test] async fn validate_invalid_file() { let (mut conn, mbt) = - open("../tests/fixtures/files/invalid/invalid_zoomed_world_cities.mbtiles").await; + open("../tests/fixtures/files/invalid_zoomed_world_cities.mbtiles").await; let result = mbt.check_agg_tiles_hashes(&mut conn).await; assert!(matches!(result, Err(MbtError::AggHashMismatch(..)))); } diff --git a/martin-mbtiles/src/tile_copier.rs b/martin-mbtiles/src/tile_copier.rs index 6827f410f..8e8ced1b5 100644 --- a/martin-mbtiles/src/tile_copier.rs +++ b/martin-mbtiles/src/tile_copier.rs @@ -603,14 +603,14 @@ mod tests { #[actix_rt::test] async fn copy_flat_tables() -> MbtResult<()> { - let src = PathBuf::from("../tests/fixtures/files/world_cities.mbtiles"); + let src = PathBuf::from("../tests/fixtures/mbtiles/world_cities.mbtiles"); let dst = PathBuf::from("file:copy_flat_tables_mem_db?mode=memory&cache=shared"); verify_copy_all(src, dst, None, Flat).await } #[actix_rt::test] async fn copy_flat_from_flat_with_hash_tables() -> MbtResult<()> { - let src = PathBuf::from("../tests/fixtures/files/zoomed_world_cities.mbtiles"); + let src = PathBuf::from("../tests/fixtures/mbtiles/zoomed_world_cities.mbtiles"); let dst = PathBuf::from( "file:copy_flat_from_flat_with_hash_tables_mem_db?mode=memory&cache=shared", ); @@ -619,7 +619,7 @@ mod tests { #[actix_rt::test] async fn copy_flat_from_normalized_tables() -> MbtResult<()> { - let src = PathBuf::from("../tests/fixtures/files/geography-class-png.mbtiles"); + let src = PathBuf::from("../tests/fixtures/mbtiles/geography-class-png.mbtiles"); let dst = PathBuf::from("file:copy_flat_from_normalized_tables_mem_db?mode=memory&cache=shared"); verify_copy_all(src, dst, Some(Flat), Flat).await @@ -627,14 +627,14 @@ mod tests { #[actix_rt::test] async fn copy_flat_with_hash_tables() -> MbtResult<()> { - let src = PathBuf::from("../tests/fixtures/files/zoomed_world_cities.mbtiles"); + let src = PathBuf::from("../tests/fixtures/mbtiles/zoomed_world_cities.mbtiles"); let dst = PathBuf::from("file:copy_flat_with_hash_tables_mem_db?mode=memory&cache=shared"); verify_copy_all(src, dst, None, FlatWithHash).await } #[actix_rt::test] async fn copy_flat_with_hash_from_flat_tables() -> MbtResult<()> { - let src = PathBuf::from("../tests/fixtures/files/world_cities.mbtiles"); + let src = PathBuf::from("../tests/fixtures/mbtiles/world_cities.mbtiles"); let dst = PathBuf::from( "file:copy_flat_with_hash_from_flat_tables_mem_db?mode=memory&cache=shared", ); @@ -643,7 +643,7 @@ mod tests { #[actix_rt::test] async fn copy_flat_with_hash_from_normalized_tables() -> MbtResult<()> { - let src = PathBuf::from("../tests/fixtures/files/geography-class-png.mbtiles"); + let src = PathBuf::from("../tests/fixtures/mbtiles/geography-class-png.mbtiles"); let dst = PathBuf::from( "file:copy_flat_with_hash_from_normalized_tables_mem_db?mode=memory&cache=shared", ); @@ -652,14 +652,14 @@ mod tests { #[actix_rt::test] async fn copy_normalized_tables() -> MbtResult<()> { - let src = PathBuf::from("../tests/fixtures/files/geography-class-png.mbtiles"); + let src = PathBuf::from("../tests/fixtures/mbtiles/geography-class-png.mbtiles"); let dst = PathBuf::from("file:copy_normalized_tables_mem_db?mode=memory&cache=shared"); verify_copy_all(src, dst, None, Normalized).await } #[actix_rt::test] async fn copy_normalized_from_flat_tables() -> MbtResult<()> { - let src = PathBuf::from("../tests/fixtures/files/world_cities.mbtiles"); + let src = PathBuf::from("../tests/fixtures/mbtiles/world_cities.mbtiles"); let dst = PathBuf::from("file:copy_normalized_from_flat_tables_mem_db?mode=memory&cache=shared"); verify_copy_all(src, dst, Some(Normalized), Normalized).await @@ -667,7 +667,7 @@ mod tests { #[actix_rt::test] async fn copy_normalized_from_flat_with_hash_tables() -> MbtResult<()> { - let src = PathBuf::from("../tests/fixtures/files/zoomed_world_cities.mbtiles"); + let src = PathBuf::from("../tests/fixtures/mbtiles/zoomed_world_cities.mbtiles"); let dst = PathBuf::from( "file:copy_normalized_from_flat_with_hash_tables_mem_db?mode=memory&cache=shared", ); @@ -676,7 +676,7 @@ mod tests { #[actix_rt::test] async fn copy_with_min_max_zoom() -> MbtResult<()> { - let src = PathBuf::from("../tests/fixtures/files/world_cities.mbtiles"); + let src = PathBuf::from("../tests/fixtures/mbtiles/world_cities.mbtiles"); let dst = PathBuf::from("file:copy_with_min_max_zoom_mem_db?mode=memory&cache=shared"); let opt = TileCopierOptions::new(src, dst) .min_zoom(Some(2)) @@ -686,7 +686,7 @@ mod tests { #[actix_rt::test] async fn copy_with_zoom_levels() -> MbtResult<()> { - let src = PathBuf::from("../tests/fixtures/files/world_cities.mbtiles"); + let src = PathBuf::from("../tests/fixtures/mbtiles/world_cities.mbtiles"); let dst = PathBuf::from("file:copy_with_zoom_levels_mem_db?mode=memory&cache=shared"); let opt = TileCopierOptions::new(src, dst) .min_zoom(Some(2)) @@ -697,11 +697,11 @@ mod tests { #[actix_rt::test] async fn copy_with_diff_with_file() -> MbtResult<()> { - let src = PathBuf::from("../tests/fixtures/files/geography-class-jpg.mbtiles"); + let src = PathBuf::from("../tests/fixtures/mbtiles/geography-class-jpg.mbtiles"); let dst = PathBuf::from("file:copy_with_diff_with_file_mem_db?mode=memory&cache=shared"); let diff_file = - PathBuf::from("../tests/fixtures/files/geography-class-jpg-modified.mbtiles"); + PathBuf::from("../tests/fixtures/mbtiles/geography-class-jpg-modified.mbtiles"); let copy_opts = TileCopierOptions::new(src.clone(), dst.clone()).diff_with_file(diff_file.clone()); @@ -744,10 +744,10 @@ mod tests { #[actix_rt::test] async fn ignore_dst_type_when_copy_to_existing() -> MbtResult<()> { - let src_file = PathBuf::from("../tests/fixtures/files/world_cities_modified.mbtiles"); + let src_file = PathBuf::from("../tests/fixtures/mbtiles/world_cities_modified.mbtiles"); // Copy the dst file to an in-memory DB - let dst_file = PathBuf::from("../tests/fixtures/files/world_cities.mbtiles"); + let dst_file = PathBuf::from("../tests/fixtures/mbtiles/world_cities.mbtiles"); let dst = PathBuf::from( "file:ignore_dst_type_when_copy_to_existing_mem_db?mode=memory&cache=shared", ); @@ -761,8 +761,8 @@ mod tests { #[actix_rt::test] async fn copy_to_existing_abort_mode() { - let src = PathBuf::from("../tests/fixtures/files/world_cities_modified.mbtiles"); - let dst = PathBuf::from("../tests/fixtures/files/world_cities.mbtiles"); + let src = PathBuf::from("../tests/fixtures/mbtiles/world_cities_modified.mbtiles"); + let dst = PathBuf::from("../tests/fixtures/mbtiles/world_cities.mbtiles"); let copy_opts = TileCopierOptions::new(src.clone(), dst.clone()).on_duplicate(CopyDuplicateMode::Abort); @@ -775,10 +775,10 @@ mod tests { #[actix_rt::test] async fn copy_to_existing_override_mode() -> MbtResult<()> { - let src_file = PathBuf::from("../tests/fixtures/files/world_cities_modified.mbtiles"); + let src_file = PathBuf::from("../tests/fixtures/mbtiles/world_cities_modified.mbtiles"); // Copy the dst file to an in-memory DB - let dst_file = PathBuf::from("../tests/fixtures/files/world_cities.mbtiles"); + let dst_file = PathBuf::from("../tests/fixtures/mbtiles/world_cities.mbtiles"); let dst = PathBuf::from("file:copy_to_existing_override_mode_mem_db?mode=memory&cache=shared"); @@ -804,10 +804,10 @@ mod tests { #[actix_rt::test] async fn copy_to_existing_ignore_mode() -> MbtResult<()> { - let src_file = PathBuf::from("../tests/fixtures/files/world_cities_modified.mbtiles"); + let src_file = PathBuf::from("../tests/fixtures/mbtiles/world_cities_modified.mbtiles"); // Copy the dst file to an in-memory DB - let dst_file = PathBuf::from("../tests/fixtures/files/world_cities.mbtiles"); + let dst_file = PathBuf::from("../tests/fixtures/mbtiles/world_cities.mbtiles"); let dst = PathBuf::from("file:copy_to_existing_ignore_mode_mem_db?mode=memory&cache=shared"); @@ -858,7 +858,7 @@ mod tests { #[actix_rt::test] async fn apply_flat_diff_file() -> MbtResult<()> { // Copy the src file to an in-memory DB - let src_file = PathBuf::from("../tests/fixtures/files/world_cities.mbtiles"); + let src_file = PathBuf::from("../tests/fixtures/mbtiles/world_cities.mbtiles"); let src = PathBuf::from("file:apply_flat_diff_file_mem_db?mode=memory&cache=shared"); let mut src_conn = TileCopierOptions::new(src_file.clone(), src.clone()) @@ -866,11 +866,11 @@ mod tests { .await?; // Apply diff to the src data in in-memory DB - let diff_file = PathBuf::from("../tests/fixtures/files/world_cities_diff.mbtiles"); + let diff_file = PathBuf::from("../tests/fixtures/mbtiles/world_cities_diff.mbtiles"); apply_mbtiles_diff(src, diff_file).await?; // Verify the data is the same as the file the diff was generated from - let path = "../tests/fixtures/files/world_cities_modified.mbtiles"; + let path = "../tests/fixtures/mbtiles/world_cities_modified.mbtiles"; attach_other_db(&mut src_conn, path).await?; assert!( @@ -886,7 +886,7 @@ mod tests { #[actix_rt::test] async fn apply_normalized_diff_file() -> MbtResult<()> { // Copy the src file to an in-memory DB - let src_file = PathBuf::from("../tests/fixtures/files/geography-class-jpg.mbtiles"); + let src_file = PathBuf::from("../tests/fixtures/mbtiles/geography-class-jpg.mbtiles"); let src = PathBuf::from("file:apply_normalized_diff_file_mem_db?mode=memory&cache=shared"); let mut src_conn = TileCopierOptions::new(src_file.clone(), src.clone()) @@ -894,11 +894,11 @@ mod tests { .await?; // Apply diff to the src data in in-memory DB - let diff_file = PathBuf::from("../tests/fixtures/files/geography-class-jpg-diff.mbtiles"); + let diff_file = PathBuf::from("../tests/fixtures/mbtiles/geography-class-jpg-diff.mbtiles"); apply_mbtiles_diff(src, diff_file).await?; // Verify the data is the same as the file the diff was generated from - let path = "../tests/fixtures/files/geography-class-jpg-modified.mbtiles"; + let path = "../tests/fixtures/mbtiles/geography-class-jpg-modified.mbtiles"; attach_other_db(&mut src_conn, path).await?; assert!( diff --git a/tests/config.yaml b/tests/config.yaml index 1f9dc1c66..471f31428 100644 --- a/tests/config.yaml +++ b/tests/config.yaml @@ -162,7 +162,7 @@ postgres: pmtiles: sources: - pmt: tests/fixtures/files/stamen_toner__raster_CC-BY+ODbL_z3.pmtiles + pmt: tests/fixtures/pmtiles/stamen_toner__raster_CC-BY+ODbL_z3.pmtiles sprites: paths: tests/fixtures/sprites/src1 diff --git a/tests/expected/generated_config.yaml b/tests/expected/generated_config.yaml index c51f1f02f..e8229fd9a 100644 --- a/tests/expected/generated_config.yaml +++ b/tests/expected/generated_config.yaml @@ -133,23 +133,27 @@ postgres: schema: public function: function_zxy_row_key pmtiles: - paths: tests/fixtures/files + paths: + - tests/fixtures/mbtiles + - tests/fixtures/pmtiles sources: - png: tests/fixtures/files/png.pmtiles - stamen_toner__raster_CC-BY-ODbL_z3: tests/fixtures/files/stamen_toner__raster_CC-BY+ODbL_z3.pmtiles - webp2: tests/fixtures/files/webp2.pmtiles + png: tests/fixtures/pmtiles/png.pmtiles + stamen_toner__raster_CC-BY-ODbL_z3: tests/fixtures/pmtiles/stamen_toner__raster_CC-BY+ODbL_z3.pmtiles + webp2: tests/fixtures/pmtiles/webp2.pmtiles mbtiles: - paths: tests/fixtures/files + paths: + - tests/fixtures/mbtiles + - tests/fixtures/pmtiles sources: - geography-class-jpg: tests/fixtures/files/geography-class-jpg.mbtiles - geography-class-jpg-diff: tests/fixtures/files/geography-class-jpg-diff.mbtiles - geography-class-jpg-modified: tests/fixtures/files/geography-class-jpg-modified.mbtiles - geography-class-png: tests/fixtures/files/geography-class-png.mbtiles - geography-class-png-no-bounds: tests/fixtures/files/geography-class-png-no-bounds.mbtiles - json: tests/fixtures/files/json.mbtiles - uncompressed_mvt: tests/fixtures/files/uncompressed_mvt.mbtiles - webp: tests/fixtures/files/webp.mbtiles - world_cities: tests/fixtures/files/world_cities.mbtiles - world_cities_diff: tests/fixtures/files/world_cities_diff.mbtiles - world_cities_modified: tests/fixtures/files/world_cities_modified.mbtiles - zoomed_world_cities: tests/fixtures/files/zoomed_world_cities.mbtiles + geography-class-jpg: tests/fixtures/mbtiles/geography-class-jpg.mbtiles + geography-class-jpg-diff: tests/fixtures/mbtiles/geography-class-jpg-diff.mbtiles + geography-class-jpg-modified: tests/fixtures/mbtiles/geography-class-jpg-modified.mbtiles + geography-class-png: tests/fixtures/mbtiles/geography-class-png.mbtiles + geography-class-png-no-bounds: tests/fixtures/mbtiles/geography-class-png-no-bounds.mbtiles + json: tests/fixtures/mbtiles/json.mbtiles + uncompressed_mvt: tests/fixtures/mbtiles/uncompressed_mvt.mbtiles + webp: tests/fixtures/mbtiles/webp.mbtiles + world_cities: tests/fixtures/mbtiles/world_cities.mbtiles + world_cities_diff: tests/fixtures/mbtiles/world_cities_diff.mbtiles + world_cities_modified: tests/fixtures/mbtiles/world_cities_modified.mbtiles + zoomed_world_cities: tests/fixtures/mbtiles/zoomed_world_cities.mbtiles diff --git a/tests/expected/given_config.yaml b/tests/expected/given_config.yaml index 033c66283..6ba51282e 100644 --- a/tests/expected/given_config.yaml +++ b/tests/expected/given_config.yaml @@ -159,7 +159,7 @@ postgres: - 90.0 pmtiles: sources: - pmt: tests/fixtures/files/stamen_toner__raster_CC-BY+ODbL_z3.pmtiles + pmt: tests/fixtures/pmtiles/stamen_toner__raster_CC-BY+ODbL_z3.pmtiles sprites: paths: tests/fixtures/sprites/src1 sources: diff --git a/tests/expected/mbtiles/copy_diff.txt b/tests/expected/mbtiles/copy_diff.txt index e69de29bb..4fc7c848d 100644 --- a/tests/expected/mbtiles/copy_diff.txt +++ b/tests/expected/mbtiles/copy_diff.txt @@ -0,0 +1 @@ +[INFO ] Creating new metadata value agg_tiles_hash = C7E2E5A9BA04693994DB1F57D1DF5646 in tests/temp/world_cities_diff.mbtiles diff --git a/tests/expected/mbtiles/copy_diff2.txt b/tests/expected/mbtiles/copy_diff2.txt index e69de29bb..cddb2637d 100644 --- a/tests/expected/mbtiles/copy_diff2.txt +++ b/tests/expected/mbtiles/copy_diff2.txt @@ -0,0 +1 @@ +[INFO ] Creating new metadata value agg_tiles_hash = D41D8CD98F00B204E9800998ECF8427E in tests/temp/world_cities_diff_modified.mbtiles diff --git a/tests/expected/mbtiles/validate-bad.txt b/tests/expected/mbtiles/validate-bad.txt new file mode 100644 index 000000000..242a6aaaa --- /dev/null +++ b/tests/expected/mbtiles/validate-bad.txt @@ -0,0 +1,3 @@ +[INFO ] Quick integrity check passed for ./tests/fixtures/files/bad_hash.mbtiles +[INFO ] All tile hashes are valid for ./tests/fixtures/files/bad_hash.mbtiles +[ERROR] Computed aggregate tiles hash D4E1030D57751A0B45A28A71267E46B8 does not match tile data in metadata CAFEC0DEDEADBEEFDEADBEEFDEADBEEF for MBTile file ./tests/fixtures/files/bad_hash.mbtiles diff --git a/tests/expected/mbtiles/validate-fix.txt b/tests/expected/mbtiles/validate-fix.txt new file mode 100644 index 000000000..ded275f11 --- /dev/null +++ b/tests/expected/mbtiles/validate-fix.txt @@ -0,0 +1,3 @@ +[INFO ] Quick integrity check passed for tests/temp/fix_bad_hash.mbtiles +[INFO ] All tile hashes are valid for tests/temp/fix_bad_hash.mbtiles +[INFO ] Updating agg_tiles_hash from CAFEC0DEDEADBEEFDEADBEEFDEADBEEF to D4E1030D57751A0B45A28A71267E46B8 in tests/temp/fix_bad_hash.mbtiles diff --git a/tests/expected/mbtiles/validate-fix2.txt b/tests/expected/mbtiles/validate-fix2.txt new file mode 100644 index 000000000..250ba58e6 --- /dev/null +++ b/tests/expected/mbtiles/validate-fix2.txt @@ -0,0 +1,3 @@ +[INFO ] Quick integrity check passed for tests/temp/fix_bad_hash.mbtiles +[INFO ] All tile hashes are valid for tests/temp/fix_bad_hash.mbtiles +[INFO ] The agg_tiles_hashes=D4E1030D57751A0B45A28A71267E46B8 has been verified for tests/temp/fix_bad_hash.mbtiles diff --git a/tests/expected/mbtiles/validate-ok.txt b/tests/expected/mbtiles/validate-ok.txt new file mode 100644 index 000000000..87a15d314 --- /dev/null +++ b/tests/expected/mbtiles/validate-ok.txt @@ -0,0 +1,3 @@ +[INFO ] Quick integrity check passed for ./tests/fixtures/mbtiles/zoomed_world_cities.mbtiles +[INFO ] All tile hashes are valid for ./tests/fixtures/mbtiles/zoomed_world_cities.mbtiles +[INFO ] The agg_tiles_hashes=D4E1030D57751A0B45A28A71267E46B8 has been verified for ./tests/fixtures/mbtiles/zoomed_world_cities.mbtiles diff --git a/tests/fixtures/files/bad_hash.mbtiles b/tests/fixtures/files/bad_hash.mbtiles new file mode 100644 index 0000000000000000000000000000000000000000..33943e69916a1187237dfd4b0dd39d6309b054ab GIT binary patch literal 14848 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 wt@^^r!e9OHOOH%ZDDmQnXH4J^!Hu0jkz>Z~y=R literal 0 HcmV?d00001 diff --git a/tests/fixtures/files/invalid/invalid-tile-format.mbtiles b/tests/fixtures/files/invalid-tile-format.mbtiles similarity index 100% rename from tests/fixtures/files/invalid/invalid-tile-format.mbtiles rename to tests/fixtures/files/invalid-tile-format.mbtiles diff --git a/tests/fixtures/files/invalid/invalid.mbtiles b/tests/fixtures/files/invalid.mbtiles similarity index 100% rename from tests/fixtures/files/invalid/invalid.mbtiles rename to tests/fixtures/files/invalid.mbtiles diff --git a/tests/fixtures/files/invalid/invalid_zoomed_world_cities.mbtiles b/tests/fixtures/files/invalid_zoomed_world_cities.mbtiles similarity index 100% rename from tests/fixtures/files/invalid/invalid_zoomed_world_cities.mbtiles rename to tests/fixtures/files/invalid_zoomed_world_cities.mbtiles diff --git a/tests/fixtures/files/geography-class-jpg-diff.mbtiles b/tests/fixtures/mbtiles/geography-class-jpg-diff.mbtiles similarity index 100% rename from tests/fixtures/files/geography-class-jpg-diff.mbtiles rename to tests/fixtures/mbtiles/geography-class-jpg-diff.mbtiles diff --git a/tests/fixtures/files/geography-class-jpg-modified.mbtiles b/tests/fixtures/mbtiles/geography-class-jpg-modified.mbtiles similarity index 100% rename from tests/fixtures/files/geography-class-jpg-modified.mbtiles rename to tests/fixtures/mbtiles/geography-class-jpg-modified.mbtiles diff --git a/tests/fixtures/files/geography-class-jpg.mbtiles b/tests/fixtures/mbtiles/geography-class-jpg.mbtiles similarity index 100% rename from tests/fixtures/files/geography-class-jpg.mbtiles rename to tests/fixtures/mbtiles/geography-class-jpg.mbtiles diff --git a/tests/fixtures/files/geography-class-png-no-bounds.mbtiles b/tests/fixtures/mbtiles/geography-class-png-no-bounds.mbtiles similarity index 100% rename from tests/fixtures/files/geography-class-png-no-bounds.mbtiles rename to tests/fixtures/mbtiles/geography-class-png-no-bounds.mbtiles diff --git a/tests/fixtures/files/geography-class-png.mbtiles b/tests/fixtures/mbtiles/geography-class-png.mbtiles similarity index 100% rename from tests/fixtures/files/geography-class-png.mbtiles rename to tests/fixtures/mbtiles/geography-class-png.mbtiles diff --git a/tests/fixtures/files/json.mbtiles b/tests/fixtures/mbtiles/json.mbtiles similarity index 100% rename from tests/fixtures/files/json.mbtiles rename to tests/fixtures/mbtiles/json.mbtiles diff --git a/tests/fixtures/files/uncompressed_mvt.mbtiles b/tests/fixtures/mbtiles/uncompressed_mvt.mbtiles similarity index 100% rename from tests/fixtures/files/uncompressed_mvt.mbtiles rename to tests/fixtures/mbtiles/uncompressed_mvt.mbtiles diff --git a/tests/fixtures/files/webp.mbtiles b/tests/fixtures/mbtiles/webp.mbtiles similarity index 100% rename from tests/fixtures/files/webp.mbtiles rename to tests/fixtures/mbtiles/webp.mbtiles diff --git a/tests/fixtures/files/world_cities.mbtiles b/tests/fixtures/mbtiles/world_cities.mbtiles similarity index 100% rename from tests/fixtures/files/world_cities.mbtiles rename to tests/fixtures/mbtiles/world_cities.mbtiles diff --git a/tests/fixtures/files/world_cities_diff.mbtiles b/tests/fixtures/mbtiles/world_cities_diff.mbtiles similarity index 100% rename from tests/fixtures/files/world_cities_diff.mbtiles rename to tests/fixtures/mbtiles/world_cities_diff.mbtiles diff --git a/tests/fixtures/files/world_cities_modified.mbtiles b/tests/fixtures/mbtiles/world_cities_modified.mbtiles similarity index 100% rename from tests/fixtures/files/world_cities_modified.mbtiles rename to tests/fixtures/mbtiles/world_cities_modified.mbtiles diff --git a/tests/fixtures/files/zoomed_world_cities.mbtiles b/tests/fixtures/mbtiles/zoomed_world_cities.mbtiles similarity index 100% rename from tests/fixtures/files/zoomed_world_cities.mbtiles rename to tests/fixtures/mbtiles/zoomed_world_cities.mbtiles diff --git a/tests/fixtures/files/png.pmtiles b/tests/fixtures/pmtiles/png.pmtiles similarity index 100% rename from tests/fixtures/files/png.pmtiles rename to tests/fixtures/pmtiles/png.pmtiles diff --git a/tests/fixtures/files/stamen_toner__raster_CC-BY+ODbL_z3.pmtiles b/tests/fixtures/pmtiles/stamen_toner__raster_CC-BY+ODbL_z3.pmtiles similarity index 100% rename from tests/fixtures/files/stamen_toner__raster_CC-BY+ODbL_z3.pmtiles rename to tests/fixtures/pmtiles/stamen_toner__raster_CC-BY+ODbL_z3.pmtiles diff --git a/tests/fixtures/files/webp2.pmtiles b/tests/fixtures/pmtiles/webp2.pmtiles similarity index 100% rename from tests/fixtures/files/webp2.pmtiles rename to tests/fixtures/pmtiles/webp2.pmtiles diff --git a/tests/mb_server_test.rs b/tests/mb_server_test.rs index 912c92a0f..94e1d1dff 100644 --- a/tests/mb_server_test.rs +++ b/tests/mb_server_test.rs @@ -34,10 +34,10 @@ fn test_get(path: &str) -> TestRequest { const CONFIG: &str = indoc! {" mbtiles: sources: - m_json: tests/fixtures/files/json.mbtiles - m_mvt: tests/fixtures/files/world_cities.mbtiles - m_raw_mvt: tests/fixtures/files/uncompressed_mvt.mbtiles - m_webp: tests/fixtures/files/webp.mbtiles + m_json: tests/fixtures/mbtiles/json.mbtiles + m_mvt: tests/fixtures/mbtiles/world_cities.mbtiles + m_raw_mvt: tests/fixtures/mbtiles/uncompressed_mvt.mbtiles + m_webp: tests/fixtures/mbtiles/webp.mbtiles "}; #[actix_rt::test] diff --git a/tests/pg_table_source_test.rs b/tests/pg_table_source_test.rs index 4207138d4..5bba0b85b 100644 --- a/tests/pg_table_source_test.rs +++ b/tests/pg_table_source_test.rs @@ -113,5 +113,5 @@ async fn table_source_schemas() { functions: false "}); let sources = mock_sources(cfg).await.0; - assert_eq!(sources.keys().collect::>(), vec!["MixPoints"],); + assert_eq!(sources.keys().collect::>(), vec!["MixPoints"]); } diff --git a/tests/pmt_server_test.rs b/tests/pmt_server_test.rs index bcb81da41..916601a87 100644 --- a/tests/pmt_server_test.rs +++ b/tests/pmt_server_test.rs @@ -34,12 +34,12 @@ fn test_get(path: &str) -> TestRequest { const CONFIG: &str = indoc! {" pmtiles: sources: - p_png: tests/fixtures/files/stamen_toner__raster_CC-BY+ODbL_z3.pmtiles + p_png: tests/fixtures/pmtiles/stamen_toner__raster_CC-BY+ODbL_z3.pmtiles "}; #[actix_rt::test] async fn pmt_get_catalog() { - let path = "pmtiles: tests/fixtures/files/stamen_toner__raster_CC-BY+ODbL_z3.pmtiles"; + let path = "pmtiles: tests/fixtures/pmtiles/stamen_toner__raster_CC-BY+ODbL_z3.pmtiles"; let app = create_app! { path }; let req = test_get("/catalog").to_request(); diff --git a/tests/test.sh b/tests/test.sh index 846e1a3de..ab47e712a 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -150,7 +150,7 @@ echo "Test auto configured Martin" TEST_OUT_DIR="$(dirname "$0")/output/auto" mkdir -p "$TEST_OUT_DIR" -ARG=(--default-srid 900913 --disable-bounds --save-config "$(dirname "$0")/output/generated_config.yaml" tests/fixtures/files) +ARG=(--default-srid 900913 --disable-bounds --save-config "$(dirname "$0")/output/generated_config.yaml" tests/fixtures/mbtiles tests/fixtures/pmtiles) set -x $MARTIN_BIN "${ARG[@]}" 2>&1 | tee test_log_1.txt & PROCESS_ID=`jobs -p` @@ -289,21 +289,34 @@ if [[ "$MBTILES_BIN" != "-" ]]; then $MBTILES_BIN --help 2>&1 | tee "$TEST_OUT_DIR/help.txt" $MBTILES_BIN meta-all --help 2>&1 | tee "$TEST_OUT_DIR/meta-all_help.txt" - $MBTILES_BIN meta-all ./tests/fixtures/files/world_cities.mbtiles 2>&1 | tee "$TEST_OUT_DIR/meta-all.txt" + $MBTILES_BIN meta-all ./tests/fixtures/mbtiles/world_cities.mbtiles 2>&1 | tee "$TEST_OUT_DIR/meta-all.txt" $MBTILES_BIN meta-get --help 2>&1 | tee "$TEST_OUT_DIR/meta-get_help.txt" - $MBTILES_BIN meta-get ./tests/fixtures/files/world_cities.mbtiles name 2>&1 | tee "$TEST_OUT_DIR/meta-get_name.txt" - $MBTILES_BIN meta-get ./tests/fixtures/files/world_cities.mbtiles missing_value 2>&1 | tee "$TEST_OUT_DIR/meta-get_missing_value.txt" + $MBTILES_BIN meta-get ./tests/fixtures/mbtiles/world_cities.mbtiles name 2>&1 | tee "$TEST_OUT_DIR/meta-get_name.txt" + $MBTILES_BIN meta-get ./tests/fixtures/mbtiles/world_cities.mbtiles missing_value 2>&1 | tee "$TEST_OUT_DIR/meta-get_missing_value.txt" + $MBTILES_BIN validate ./tests/fixtures/mbtiles/zoomed_world_cities.mbtiles 2>&1 | tee "$TEST_OUT_DIR/validate-ok.txt" + + set +e + $MBTILES_BIN validate ./tests/fixtures/files/bad_hash.mbtiles 2>&1 | tee "$TEST_OUT_DIR/validate-bad.txt" + if [[ $? -eq 0 ]]; then + echo "ERROR: validate with bad_hash should have failed" + exit 1 + fi + set -e + + cp ./tests/fixtures/files/bad_hash.mbtiles "$TEST_TEMP_DIR/fix_bad_hash.mbtiles" + $MBTILES_BIN validate --update-agg-tiles-hash "$TEST_TEMP_DIR/fix_bad_hash.mbtiles" 2>&1 | tee "$TEST_OUT_DIR/validate-fix.txt" + $MBTILES_BIN validate "$TEST_TEMP_DIR/fix_bad_hash.mbtiles" 2>&1 | tee "$TEST_OUT_DIR/validate-fix2.txt" # Create diff file $MBTILES_BIN copy \ - ./tests/fixtures/files/world_cities.mbtiles \ + ./tests/fixtures/mbtiles/world_cities.mbtiles \ "$TEST_TEMP_DIR/world_cities_diff.mbtiles" \ - --diff-with-file ./tests/fixtures/files/world_cities_modified.mbtiles \ + --diff-with-file ./tests/fixtures/mbtiles/world_cities_modified.mbtiles \ 2>&1 | tee "$TEST_OUT_DIR/copy_diff.txt" if command -v sqlite3 > /dev/null; then # Apply this diff to the original version of the file - cp ./tests/fixtures/files/world_cities.mbtiles "$TEST_TEMP_DIR/world_cities_copy.mbtiles" + cp ./tests/fixtures/mbtiles/world_cities.mbtiles "$TEST_TEMP_DIR/world_cities_copy.mbtiles" sqlite3 "$TEST_TEMP_DIR/world_cities_copy.mbtiles" \ -bail \ @@ -315,7 +328,7 @@ if [[ "$MBTILES_BIN" != "-" ]]; then # Ensure that applying the diff resulted in the modified version of the file $MBTILES_BIN copy \ --diff-with-file "$TEST_TEMP_DIR/world_cities_copy.mbtiles" \ - ./tests/fixtures/files/world_cities_modified.mbtiles \ + ./tests/fixtures/mbtiles/world_cities_modified.mbtiles \ "$TEST_TEMP_DIR/world_cities_diff_modified.mbtiles" \ 2>&1 | tee "$TEST_OUT_DIR/copy_diff2.txt" From 06ec44a74bee62d419c2df7ec7d53a56bd3e7cfc Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Fri, 29 Sep 2023 17:18:57 -0400 Subject: [PATCH 006/108] update lock --- Cargo.lock | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0fadd1617..6b5ff6238 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -298,9 +298,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" -version = "0.6.0" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bff2cf94a3dbe2d57cbd56485e1bd7436455058034d6c2d47be51d4e5e4bc6ab" +checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" dependencies = [ "anstyle", "anstyle-parse", @@ -336,9 +336,9 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "3.0.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0238ca56c96dfa37bdf7c373c8886dd591322500aceeeccdb2216fe06dc2f796" +checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" dependencies = [ "anstyle", "windows-sys", @@ -1399,9 +1399,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" dependencies = [ "ahash", "allocator-api2", @@ -1413,7 +1413,7 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" dependencies = [ - "hashbrown 0.14.0", + "hashbrown 0.14.1", ] [[package]] @@ -1559,12 +1559,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.0.1" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad227c3af19d4914570ad36d30409928b75967c298feb9ea1969db3a610bb14e" +checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" dependencies = [ "equivalent", - "hashbrown 0.14.0", + "hashbrown 0.14.1", ] [[package]] @@ -2774,7 +2774,7 @@ version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" dependencies = [ - "indexmap 2.0.1", + "indexmap 2.0.2", "itoa", "ryu", "serde", @@ -2819,7 +2819,7 @@ version = "0.9.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a49e178e4452f45cb61d0cd8cebc1b0fafd3e41929e996cef79aa3aca91f574" dependencies = [ - "indexmap 2.0.1", + "indexmap 2.0.2", "itoa", "ryu", "serde", @@ -3025,7 +3025,7 @@ dependencies = [ "futures-util", "hashlink", "hex", - "indexmap 2.0.1", + "indexmap 2.0.2", "log", "memchr", "native-tls", From cd584faa3026f1bd11b6fe8c52cef1f3c99883b7 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Fri, 29 Sep 2023 20:40:32 -0400 Subject: [PATCH 007/108] mbtiles: remove tls features, CI streamlining (#908) --- .github/workflows/ci.yml | 26 +++--- Cargo.lock | 152 +----------------------------------- Cargo.toml | 4 +- martin-mbtiles/Cargo.toml | 14 ++-- martin-mbtiles/README.md | 17 +++- martin-mbtiles/src/lib.rs | 13 +-- martin-tile-utils/README.md | 15 ++++ martin/Cargo.toml | 2 +- 8 files changed, 61 insertions(+), 182 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 39678e3cb..f7a8c6d94 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,9 +58,8 @@ jobs: set -x cargo fmt --all -- --check cargo clippy --package martin-tile-utils -- -D warnings + cargo clippy --package martin-mbtiles --no-default-features -- -D warnings cargo clippy --package martin-mbtiles -- -D warnings - cargo clippy --package martin-mbtiles --no-default-features --features native-tls -- -D warnings - cargo clippy --package martin-mbtiles --no-default-features --features rustls -- -D warnings cargo clippy --package martin -- -D warnings cargo clippy --package martin --features vendored-openssl -- -D warnings cargo clippy --package martin --features bless-tests -- -D warnings @@ -73,16 +72,16 @@ jobs: - name: Build (native) if: matrix.cross != 'true' run: | - cargo build --release --target ${{ matrix.target }} --features=ssl --package martin - cargo build --release --target ${{ matrix.target }} --features=cli --package martin-mbtiles + cargo build --release --target ${{ matrix.target }} --package martin --features=ssl --package martin + cargo build --release --target ${{ matrix.target }} --package martin-mbtiles - name: Build (cross - aarch64-apple-darwin) if: matrix.target == 'aarch64-apple-darwin' run: | rustup target add "${{ matrix.target }}" # compile without debug symbols because stripping them with `strip` does not work cross-platform export RUSTFLAGS='-C link-arg=-s' - cargo build --release --target ${{ matrix.target }} --features=vendored-openssl --package martin - cargo build --release --target ${{ matrix.target }} --no-default-features --features=rustls,cli --package martin-mbtiles + cargo build --release --target ${{ matrix.target }} --package martin --features=vendored-openssl + cargo build --release --target ${{ matrix.target }} --package martin-mbtiles - name: Build (cross - aarch64-unknown-linux-gnu) if: matrix.target == 'aarch64-unknown-linux-gnu' run: | @@ -90,8 +89,8 @@ jobs: rustup target add "${{ matrix.target }}" # compile without debug symbols because stripping them with `strip` does not work cross-platform export RUSTFLAGS='-C link-arg=-s -C linker=aarch64-linux-gnu-gcc' - cargo build --release --target ${{ matrix.target }} --features=vendored-openssl --package martin - cargo build --release --target ${{ matrix.target }} --no-default-features --features=rustls,cli --package martin-mbtiles + cargo build --release --target ${{ matrix.target }} --package martin --features=vendored-openssl + cargo build --release --target ${{ matrix.target }} --package martin-mbtiles - name: Build (debian package) if: matrix.target == 'debian-x86_64' run: | @@ -152,10 +151,12 @@ jobs: run: | set -x cargo test --package martin-tile-utils + cargo test --package martin-mbtiles --no-default-features cargo test --package martin-mbtiles - cargo test --package martin-mbtiles --no-default-features --features rustls + cargo test --package martin cargo test --package martin --features vendored-openssl cargo test --doc + RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --workspace cargo clean env: DATABASE_URL: ${{ steps.pg.outputs.connection-uri }} @@ -297,12 +298,7 @@ jobs: echo "Same but as base64 to prevent GitHub obfuscation (this is not a secret):" echo "$DATABASE_URL" | base64 set -x - cargo test --package martin-tile-utils - cargo test --package martin-mbtiles - cargo test --package martin-mbtiles --no-default-features --features rustls - cargo test --package martin --features vendored-openssl - cargo test --doc - RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --workspace + cargo test --package martin cargo clean env: DATABASE_URL: postgres://${{ env.PGUSER }}:${{ env.PGUSER }}@${{ env.PGHOST }}:${{ job.services.postgres.ports[5432] }}/${{ env.PGDATABASE }}?sslmode=${{ matrix.sslmode }} diff --git a/Cargo.lock b/Cargo.lock index 6b5ff6238..33a2cde97 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -687,22 +687,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "core-foundation" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" - [[package]] name = "cpufeatures" version = "0.2.9" @@ -1794,7 +1778,7 @@ dependencies = [ [[package]] name = "martin-mbtiles" -version = "0.4.0" +version = "0.5.0" dependencies = [ "actix-rt", "anyhow", @@ -1903,24 +1887,6 @@ dependencies = [ "serde", ] -[[package]] -name = "native-tls" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" -dependencies = [ - "lazy_static", - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - [[package]] name = "nom" version = "7.1.3" @@ -2047,12 +2013,6 @@ dependencies = [ "syn 2.0.37", ] -[[package]] -name = "openssl-probe" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" - [[package]] name = "openssl-src" version = "300.1.5+3.1.3" @@ -2539,21 +2499,6 @@ dependencies = [ "bytemuck", ] -[[package]] -name = "ring" -version = "0.16.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" -dependencies = [ - "cc", - "libc", - "once_cell", - "spin 0.5.2", - "untrusted", - "web-sys", - "winapi", -] - [[package]] name = "roxmltree" version = "0.18.0" @@ -2622,9 +2567,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.14" +version = "0.38.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "747c788e9ce8e92b12cd485c49ddf90723550b654b32508f979b71a7b1ecda4f" +checksum = "d2f9da0cbd88f9f09e7814e388301c8414c51c62aa6ce1e4b5c551d49d96e531" dependencies = [ "bitflags 2.4.0", "errno", @@ -2633,36 +2578,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "rustls" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" -dependencies = [ - "ring", - "rustls-webpki", - "sct", -] - -[[package]] -name = "rustls-pemfile" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" -dependencies = [ - "base64", -] - -[[package]] -name = "rustls-webpki" -version = "0.101.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "rustybuzz" version = "0.7.0" @@ -2694,54 +2609,12 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "schannel" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" -dependencies = [ - "windows-sys", -] - [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sct" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "security-framework" -version = "2.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "semver" version = "1.0.19" @@ -3028,12 +2901,9 @@ dependencies = [ "indexmap 2.0.2", "log", "memchr", - "native-tls", "once_cell", "paste", "percent-encoding", - "rustls", - "rustls-pemfile", "serde", "serde_json", "sha2", @@ -3044,7 +2914,6 @@ dependencies = [ "tokio-stream", "tracing", "url", - "webpki-roots", ] [[package]] @@ -3675,12 +3544,6 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa" -[[package]] -name = "untrusted" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" - [[package]] name = "url" version = "2.4.1" @@ -3863,15 +3726,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "webpki-roots" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b291546d5d9d1eab74f069c77749f2cb8504a12caa20f0f2de93ddbf6f411888" -dependencies = [ - "rustls-webpki", -] - [[package]] name = "weezl" version = "0.1.7" diff --git a/Cargo.toml b/Cargo.toml index acc78dcba..9cc4e70e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ indoc = "2" itertools = "0.11" json-patch = "1.1" log = "0.4" -martin-mbtiles = { path = "./martin-mbtiles", version = "0.4.0", default-features = false, features = ["native-tls"] } # disable CLI tools +martin-mbtiles = { path = "./martin-mbtiles", version = "0.5.0", default-features = false } martin-tile-utils = { path = "./martin-tile-utils", version = "0.1.0" } num_cpus = "1" openssl = "0.10" @@ -47,7 +47,7 @@ serde_json = "1" serde_yaml = "0.9" spreet = { version = "0.8", default-features = false } sqlite-hashes = "0.3" -sqlx = { version = "0.7", features = ["sqlite"] } +sqlx = { version = "0.7", features = ["sqlite", "runtime-tokio"] } subst = { version = "0.3", features = ["yaml"] } thiserror = "1" tilejson = "0.3" diff --git a/martin-mbtiles/Cargo.toml b/martin-mbtiles/Cargo.toml index fd746f043..ef6a59111 100644 --- a/martin-mbtiles/Cargo.toml +++ b/martin-mbtiles/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "martin-mbtiles" -version = "0.4.0" +version = "0.5.0" authors = ["Yuri Astrakhan ", "MapLibre contributors"] description = "A simple low-level MbTiles access and processing library, with some tile format detection and other relevant heuristics." keywords = ["mbtiles", "maps", "tiles", "mvt", "tilejson"] @@ -10,12 +10,8 @@ repository.workspace = true rust-version.workspace = true [features] -# TODO: Disable "cli" feature in default builds -default = ["cli", "native-tls"] -cli = ["dep:anyhow", "dep:clap", "dep:env_logger", "dep:tokio"] -# One of the following two must be used -native-tls = ["sqlx/runtime-tokio-native-tls"] -rustls = ["sqlx/runtime-tokio-rustls"] +default = ["cli"] +cli = ["dep:anyhow", "dep:clap", "dep:env_logger", "dep:serde_yaml", "dep:tokio"] [dependencies] futures.workspace = true @@ -23,6 +19,7 @@ log.workspace = true martin-tile-utils.workspace = true serde.workspace = true serde_json.workspace = true +sqlite-hashes.workspace = true sqlx.workspace = true thiserror.workspace = true tilejson.workspace = true @@ -31,8 +28,7 @@ tilejson.workspace = true anyhow = { workspace = true, optional = true } clap = { workspace = true, optional = true } env_logger = { workspace = true, optional = true } -serde_yaml.workspace = true -sqlite-hashes.workspace = true +serde_yaml = { workspace = true, optional = true } tokio = { workspace = true, features = ["rt-multi-thread"], optional = true } [dev-dependencies] diff --git a/martin-mbtiles/README.md b/martin-mbtiles/README.md index d0a0484fd..04f8f5a7b 100644 --- a/martin-mbtiles/README.md +++ b/martin-mbtiles/README.md @@ -7,10 +7,25 @@ [![crates.io version](https://img.shields.io/crates/v/martin-mbtiles.svg)](https://crates.io/crates/martin-mbtiles) [![CI build](https://github.com/maplibre/martin/workflows/CI/badge.svg)](https://github.com/maplibre/martin-mbtiles/actions) -A library to help tile servers like [Martin](https://maplibre.org/martin) work with [MBTiles](https://github.com/mapbox/mbtiles-spec) files. +A library to help tile servers like [Martin](https://maplibre.org/martin) work with [MBTiles](https://github.com/mapbox/mbtiles-spec) files. When using as a lib, you may want to disable default features (i.e. the unused "cli" feature). This crate also has a small utility that allows users to interact with the `*.mbtiles` files from the command line. See [tools](https://maplibre.org/martin/tools.html) documentation for more information. ### Development Any changes to SQL commands require running of `just prepare-sqlite`. This will install `cargo sqlx` command if it is not already installed, and update the `./sqlx-data.json` file. + +## License + +Licensed under either of + +* Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or ) +* MIT license ([LICENSE-MIT](LICENSE-MIT) or ) + at your option. + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally +submitted for inclusion in the work by you, as defined in the +Apache-2.0 license, shall be dual licensed as above, without any +additional terms or conditions. diff --git a/martin-mbtiles/src/lib.rs b/martin-mbtiles/src/lib.rs index 16a9747dc..65522b564 100644 --- a/martin-mbtiles/src/lib.rs +++ b/martin-mbtiles/src/lib.rs @@ -1,12 +1,15 @@ #![allow(clippy::missing_errors_doc)] mod errors; -mod mbtiles; -mod mbtiles_pool; -mod mbtiles_queries; -mod tile_copier; - pub use errors::{MbtError, MbtResult}; + +mod mbtiles; pub use mbtiles::{IntegrityCheckType, Mbtiles, Metadata}; + +mod mbtiles_pool; pub use mbtiles_pool::MbtilesPool; + +mod tile_copier; pub use tile_copier::{apply_mbtiles_diff, CopyDuplicateMode, TileCopierOptions}; + +mod mbtiles_queries; diff --git a/martin-tile-utils/README.md b/martin-tile-utils/README.md index 813c13ec4..2bdea051d 100644 --- a/martin-tile-utils/README.md +++ b/martin-tile-utils/README.md @@ -7,3 +7,18 @@ [![CI build](https://github.com/maplibre/martin/workflows/CI/badge.svg)](https://github.com/maplibre/martin-tile-utils/actions) A library to help tile servers like [Martin](https://maplibre.org/martin) work with tile content. + +## License + +Licensed under either of + +* Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or ) +* MIT license ([LICENSE-MIT](LICENSE-MIT) or ) + at your option. + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally +submitted for inclusion in the work by you, as defined in the +Apache-2.0 license, shall be dual licensed as above, without any +additional terms or conditions. diff --git a/martin/Cargo.toml b/martin/Cargo.toml index 31bd47f4b..7127dc286 100644 --- a/martin/Cargo.toml +++ b/martin/Cargo.toml @@ -2,7 +2,7 @@ name = "martin" # Make sure to update /home/nyurik/dev/rust/martin/homebrew-formula/martin.rb version # Once the release is published with the hash -version = "0.9.0-pre.1" +version = "0.9.0-pre.3" authors = ["Stepan Kuzmin ", "Yuri Astrakhan ", "MapLibre contributors"] description = "Blazing fast and lightweight tile server with PostGIS, MBTiles, and PMTiles support" keywords = ["maps", "tiles", "mbtiles", "pmtiles", "postgis"] From 0f1bd9e9c27701824d229f8d55843b2327cf20e4 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Fri, 29 Sep 2023 20:51:40 -0400 Subject: [PATCH 008/108] disable bench workflow --- .github/workflows/bench.yml | 28 ++++++++++++++-------------- Cargo.lock | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 87d40d114..5cbec58c4 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -1,20 +1,20 @@ name: Benchmark on: - pull_request: - types: [ opened, synchronize, reopened ] - paths-ignore: - - '**.md' - - 'demo/**' - - 'docs/**' - - 'homebrew-formula/**' - push: - branches: [ main ] - paths-ignore: - - '**.md' - - 'demo/**' - - 'docs/**' - - 'homebrew-formula/**' +# pull_request: +# types: [ opened, synchronize, reopened ] +# paths-ignore: +# - '**.md' +# - 'demo/**' +# - 'docs/**' +# - 'homebrew-formula/**' +# push: +# branches: [ main ] +# paths-ignore: +# - '**.md' +# - 'demo/**' +# - 'docs/**' +# - 'homebrew-formula/**' workflow_dispatch: jobs: diff --git a/Cargo.lock b/Cargo.lock index 33a2cde97..a2373b4b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1734,7 +1734,7 @@ checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "martin" -version = "0.9.0-pre.1" +version = "0.9.0-pre.3" dependencies = [ "actix", "actix-cors", From d48ef47f2925506983e7bdf69b979595683f6034 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Fri, 29 Sep 2023 22:21:14 -0400 Subject: [PATCH 009/108] Tiny nits to help with rustls migration --- martin/Cargo.toml | 2 +- martin/src/pg/errors.rs | 12 ++++++------ martin/src/pg/tls.rs | 19 ++++++++++--------- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/martin/Cargo.toml b/martin/Cargo.toml index 7127dc286..6eb70d11e 100644 --- a/martin/Cargo.toml +++ b/martin/Cargo.toml @@ -65,10 +65,10 @@ brotli.workspace = true clap.workspace = true deadpool-postgres.workspace = true env_logger.workspace = true -json-patch.workspace = true flate2.workspace = true futures.workspace = true itertools.workspace = true +json-patch.workspace = true log.workspace = true martin-mbtiles.workspace = true martin-tile-utils.workspace = true diff --git a/martin/src/pg/errors.rs b/martin/src/pg/errors.rs index b1e9aa3de..62ddec726 100644 --- a/martin/src/pg/errors.rs +++ b/martin/src/pg/errors.rs @@ -1,4 +1,4 @@ -use deadpool_postgres::tokio_postgres::Error; +use deadpool_postgres::tokio_postgres::Error as TokioPgError; use deadpool_postgres::{BuildError, PoolError}; use semver::Version; @@ -31,7 +31,7 @@ pub enum PgError { UnknownSslMode(deadpool_postgres::tokio_postgres::config::SslMode), #[error("Postgres error while {1}: {0}")] - PostgresError(#[source] Error, &'static str), + PostgresError(#[source] TokioPgError, &'static str), #[error("Unable to build a Postgres connection pool {1}: {0}")] PostgresPoolBuildError(#[source] BuildError, String), @@ -40,7 +40,7 @@ pub enum PgError { PostgresPoolConnError(#[source] PoolError, String), #[error("Unable to parse connection string {1}: {0}")] - BadConnectionString(#[source] Error, String), + BadConnectionString(#[source] TokioPgError, String), #[error("Unable to parse PostGIS version {1}: {0}")] BadPostgisVersion(#[source] semver::Error, String), @@ -52,11 +52,11 @@ pub enum PgError { InvalidTableExtent(String, String), #[error("Error preparing a query for the tile '{1}' ({2}): {3} {0}")] - PrepareQueryError(#[source] Error, String, String, String), + PrepareQueryError(#[source] TokioPgError, String, String, String), #[error(r#"Unable to get tile {2:#} from {1}: {0}"#)] - GetTileError(#[source] Error, String, Xyz), + GetTileError(#[source] TokioPgError, String, Xyz), #[error(r#"Unable to get tile {2:#} with {:?} params from {1}: {0}"#, query_to_json(.3))] - GetTileWithQueryError(#[source] Error, String, Xyz, UrlQuery), + GetTileWithQueryError(#[source] TokioPgError, String, Xyz, UrlQuery), } diff --git a/martin/src/pg/tls.rs b/martin/src/pg/tls.rs index 96e0ac35b..06186fadd 100644 --- a/martin/src/pg/tls.rs +++ b/martin/src/pg/tls.rs @@ -12,9 +12,10 @@ use regex::Regex; use crate::pg::PgError::BadConnectionString; #[cfg(feature = "ssl")] -use crate::pg::PgError::{BadClientCertError, BadClientKeyError, UnknownSslMode}; -#[cfg(feature = "ssl")] -use crate::pg::PgError::{BadTrustedRootCertError, BuildSslConnectorError}; +use crate::pg::PgError::{ + BadClientCertError, BadClientKeyError, BadTrustedRootCertError, BuildSslConnectorError, + UnknownSslMode, +}; use crate::pg::{PgSslCerts, Result}; /// A temporary workaround for @@ -53,7 +54,7 @@ pub fn parse_conn_str(conn_str: &str) -> Result<(Config, SslModeOverride)> { #[cfg(not(feature = "ssl"))] #[allow(clippy::unnecessary_wraps)] pub fn make_connector( - _certs: &PgSslCerts, + _pg_certs: &PgSslCerts, _ssl_mode: SslModeOverride, ) -> Result { Ok(deadpool_postgres::tokio_postgres::NoTls) @@ -61,13 +62,13 @@ pub fn make_connector( #[cfg(feature = "ssl")] pub fn make_connector( - certs: &PgSslCerts, + pg_certs: &PgSslCerts, ssl_mode: SslModeOverride, ) -> Result { let (verify_ca, verify_hostname) = match ssl_mode { SslModeOverride::Unmodified(mode) => match mode { SslMode::Disable | SslMode::Prefer => (false, false), - SslMode::Require => match certs.ssl_root_cert { + SslMode::Require => match pg_certs.ssl_root_cert { // If a root CA file exists, the behavior of sslmode=require will be the same as // that of verify-ca, meaning the server certificate is validated against the CA. // For more details, check out the note about backwards compatibility in @@ -86,18 +87,18 @@ pub fn make_connector( let tls = SslMethod::tls_client(); let mut builder = SslConnector::builder(tls).map_err(BuildSslConnectorError)?; - if let (Some(cert), Some(key)) = (&certs.ssl_cert, &certs.ssl_key) { + if let (Some(cert), Some(key)) = (&pg_certs.ssl_cert, &pg_certs.ssl_key) { builder .set_certificate_file(cert, SslFiletype::PEM) .map_err(|e| BadClientCertError(e, cert.clone()))?; builder .set_private_key_file(key, SslFiletype::PEM) .map_err(|e| BadClientKeyError(e, key.clone()))?; - } else if certs.ssl_key.is_some() || certs.ssl_key.is_some() { + } else if pg_certs.ssl_key.is_some() || pg_certs.ssl_key.is_some() { warn!("SSL client certificate and key files must be set to use client certificate with Postgres. Only one of them was set."); } - if let Some(file) = &certs.ssl_root_cert { + if let Some(file) = &pg_certs.ssl_root_cert { builder .set_ca_file(file) .map_err(|e| BadTrustedRootCertError(e, file.clone()))?; From 9ed414c9ac00229c66103dc9c52ec4b53de6b4ff Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Fri, 29 Sep 2023 23:11:09 -0400 Subject: [PATCH 010/108] Use MUSL cross-build for ARM64 & dockers (#909) --- .github/workflows/ci.yml | 158 +++++++++++++++++++++++--------------- multi-platform.Dockerfile | 2 +- 2 files changed, 98 insertions(+), 62 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f7a8c6d94..e11fef128 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,9 +34,6 @@ jobs: - target: aarch64-apple-darwin os: macOS-latest cross: 'true' - - target: aarch64-unknown-linux-gnu - os: ubuntu-latest - cross: 'true' - target: debian-x86_64 os: ubuntu-latest cross: 'true' @@ -52,6 +49,7 @@ jobs: uses: actions/checkout@v4 - name: Rust Versions run: rustc --version && cargo --version + - uses: Swatinem/rust-cache@v2 - name: Lint (Linux) if: matrix.target == 'x86_64-unknown-linux-gnu' run: | @@ -69,49 +67,74 @@ jobs: run: | echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append vcpkg install openssl:x64-windows-static-md - - name: Build (native) - if: matrix.cross != 'true' - run: | - cargo build --release --target ${{ matrix.target }} --package martin --features=ssl --package martin - cargo build --release --target ${{ matrix.target }} --package martin-mbtiles - - name: Build (cross - aarch64-apple-darwin) - if: matrix.target == 'aarch64-apple-darwin' - run: | - rustup target add "${{ matrix.target }}" - # compile without debug symbols because stripping them with `strip` does not work cross-platform - export RUSTFLAGS='-C link-arg=-s' - cargo build --release --target ${{ matrix.target }} --package martin --features=vendored-openssl - cargo build --release --target ${{ matrix.target }} --package martin-mbtiles - - name: Build (cross - aarch64-unknown-linux-gnu) - if: matrix.target == 'aarch64-unknown-linux-gnu' - run: | - sudo apt-get install -y gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu - rustup target add "${{ matrix.target }}" - # compile without debug symbols because stripping them with `strip` does not work cross-platform - export RUSTFLAGS='-C link-arg=-s -C linker=aarch64-linux-gnu-gcc' - cargo build --release --target ${{ matrix.target }} --package martin --features=vendored-openssl - cargo build --release --target ${{ matrix.target }} --package martin-mbtiles - - name: Build (debian package) + - name: Build (.deb) if: matrix.target == 'debian-x86_64' run: | sudo apt-get install -y dpkg dpkg-dev liblzma-dev cargo install cargo-deb cargo deb -v -p martin --output target/debian/debian-x86_64.deb - - name: Move build artifacts - run: | mkdir -p target_releases - if [[ "${{ matrix.target }}" == "debian-x86_64" ]]; then - mv target/debian/debian-x86_64.deb target_releases + mv target/debian/debian-x86_64.deb target_releases/ + - name: Build + if: matrix.target != 'debian-x86_64' + run: | + rustup target add "${{ matrix.target }}" + if [[ "${{ runner.os }}" == "Windows" ]]; then + FEATURES="ssl" else - mv target/${{ matrix.target }}/release/martin${{ matrix.ext }} target_releases - mv target/${{ matrix.target }}/release/mbtiles${{ matrix.ext }} target_releases + FEATURES="vendored-openssl" fi + + set -x + export RUSTFLAGS='-C strip=debuginfo' + cargo build --release --target ${{ matrix.target }} --package martin-mbtiles + cargo build --release --target ${{ matrix.target }} --package martin --features=$FEATURES + + mkdir -p target_releases + mv target/${{ matrix.target }}/release/mbtiles${{ matrix.ext }} target_releases/ + mv target/${{ matrix.target }}/release/martin${{ matrix.ext }} target_releases/ - name: Save build artifacts to build-${{ matrix.target }} uses: actions/upload-artifact@v3 with: name: build-${{ matrix.target }} path: target_releases/* + build-cross: + name: Cross-platform builds + runs-on: ubuntu-latest + env: + TARGETS: "aarch64-unknown-linux-musl x86_64-unknown-linux-musl" + # TODO: aarch64-unknown-linux-gnu + steps: + - name: Checkout sources + uses: actions/checkout@v4 + - uses: Swatinem/rust-cache@v2 + - name: Install cross + run: | + cargo install cross + # Install latest cross version from git (disabled as it is probably less stable) + # cargo install cross --git https://github.com/cross-rs/cross + cross --version + - name: Build targets + run: | + for target in $TARGETS; do + echo -e "\n----------------------------------------------" + echo "Building $target" + + export "CARGO_TARGET_$(echo $target | tr 'a-z-' 'A-Z_')_RUSTFLAGS"='-C strip=debuginfo' + cross build --release --target $target --package martin-mbtiles + cross build --release --target $target --package martin --features=vendored-openssl + + mkdir -p target_releases/$target + mv target/$target/release/mbtiles target_releases/$target + mv target/$target/release/martin target_releases/$target + done + - name: Save build artifacts to build-${{ matrix.target }} + uses: actions/upload-artifact@v3 + with: + name: build-cross + path: target_releases/* + test: name: Test ${{ matrix.target }} runs-on: ${{ matrix.os }} @@ -130,6 +153,7 @@ jobs: steps: - name: Checkout sources uses: actions/checkout@v4 + - uses: Swatinem/rust-cache@v2 - name: Start postgres uses: nyurik/action-setup-postgis@v1 id: pg @@ -255,6 +279,7 @@ jobs: steps: - name: Checkout sources uses: actions/checkout@v4 + - uses: Swatinem/rust-cache@v2 - name: Setup database run: tests/fixtures/initdb.sh env: @@ -313,7 +338,7 @@ jobs: docker: name: Build docker images runs-on: ubuntu-latest - needs: [ build ] + needs: [ build, build-cross ] env: # PG_* variables are used by psql PGDATABASE: test @@ -345,6 +370,7 @@ jobs: steps: - name: Checkout sources uses: actions/checkout@v4 + - uses: Swatinem/rust-cache@v2 - name: Setup database run: tests/fixtures/initdb.sh env: @@ -362,19 +388,20 @@ jobs: install: true platforms: linux/amd64,linux/arm64 - - run: rm -rf target_releases - - name: Download build artifact build-aarch64-unknown-linux-gnu + - run: rm -rf target_releases2 + - name: Download build-cross artifacts uses: actions/download-artifact@v3 with: - name: build-aarch64-unknown-linux-gnu - path: target_releases/linux/arm64 - - name: Download build artifact build-x86_64-unknown-linux-gnu - uses: actions/download-artifact@v3 - with: - name: build-x86_64-unknown-linux-gnu - path: target_releases/linux/amd64 - - name: Reset permissions - run: chmod -R +x target_releases/ + name: build-cross + path: target_releases2 + - name: Reorganize build artifacts + run: | + chmod -R +x target_releases2/ + mkdir -p target_releases/linux/arm64 + mv target_releases2/aarch64-unknown-linux-musl/* target_releases/linux/arm64/ + mkdir -p target_releases/linux/amd64 + mv target_releases2/x86_64-unknown-linux-musl/* target_releases/linux/amd64/ + rm -rf target_releases2 - name: Build linux/arm64 Docker image id: docker_aarch64-unknown-linux-gnu @@ -457,12 +484,13 @@ jobs: - target: aarch64-apple-darwin os: ubuntu-latest name: martin-Darwin-aarch64.tar.gz - cross: 'true' sha: 'true' - - target: aarch64-unknown-linux-gnu + - target: debian-x86_64 os: ubuntu-latest - name: martin-Linux-aarch64.tar.gz - cross: 'true' + name: martin-Debian-x86_64.deb + # - target: aarch64-unknown-linux-gnu + # os: ubuntu-latest + # name: martin-Linux-aarch64.tar.gz - target: x86_64-apple-darwin os: macOS-latest name: martin-Darwin-x86_64.tar.gz @@ -474,26 +502,30 @@ jobs: - target: x86_64-unknown-linux-gnu os: ubuntu-latest name: martin-Linux-x86_64.tar.gz - - target: debian-x86_64 + # From the cross build + - target: aarch64-unknown-linux-musl os: ubuntu-latest - name: martin-Debian-x86_64.deb cross: 'true' + name: martin-Linux-aarch64-musl.tar.gz + - target: x86_64-unknown-linux-musl + os: ubuntu-latest + cross: 'true' + name: martin-Linux-x86_64-musl.tar.gz steps: - name: Checkout sources uses: actions/checkout@v4 - name: Download build artifact build-${{ matrix.target }} + if: matrix.cross != 'true' uses: actions/download-artifact@v3 with: name: build-${{ matrix.target }} path: target/ - - name: Strip symbols - # Symbol stripping does not work cross-platform - # For cross, symbols were already removed during build - if: matrix.cross != 'true' - run: | - cd target/ - strip martin${{ matrix.ext }} - strip mbtiles${{ matrix.ext }} + - name: Download cross-build artifact build-${{ matrix.target }} + if: matrix.cross == 'true' + uses: actions/download-artifact@v3 + with: + name: build-cross + path: target/ - name: Package run: | cd target/ @@ -502,11 +534,15 @@ jobs: elif [[ "${{ matrix.target }}" == "debian-x86_64" ]]; then mv debian-x86_64.deb ../${{ matrix.name }} else + if [[ "${{ matrix.cross }}" == "true" ]]; then + mv ${{ matrix.target }}/* . + fi tar czvf ../${{ matrix.name }} martin${{ matrix.ext }} mbtiles${{ matrix.ext }} fi - - name: Generate SHA-256 (MacOS) - if: matrix.sha == 'true' - run: shasum -a 256 ${{ matrix.name }} + # TODO: why is this needed and where should the result go? + # - name: Generate SHA-256 (MacOS) + # if: matrix.sha == 'true' + # run: shasum -a 256 ${{ matrix.name }} - name: Publish if: startsWith(github.ref, 'refs/tags/') uses: softprops/action-gh-release@v1 diff --git a/multi-platform.Dockerfile b/multi-platform.Dockerfile index e1fe2f4ee..b7420a3da 100644 --- a/multi-platform.Dockerfile +++ b/multi-platform.Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:latest +FROM alpine ARG TARGETPLATFORM LABEL org.opencontainers.image.description="Blazing fast and lightweight tile server with PostGIS, MBTiles, and PMTiles support" From 976a850632aae4c31191093e0f2cdac065757fbb Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Fri, 29 Sep 2023 23:13:18 -0400 Subject: [PATCH 011/108] Use rust-cache in CI --- .github/workflows/build-deploy-docs.yml | 1 + .github/workflows/grcov.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/build-deploy-docs.yml b/.github/workflows/build-deploy-docs.yml index 0a07b310d..c932c99f0 100644 --- a/.github/workflows/build-deploy-docs.yml +++ b/.github/workflows/build-deploy-docs.yml @@ -13,6 +13,7 @@ jobs: group: ${{ github.workflow }}-${{ github.ref }} steps: - uses: actions/checkout@v4 + - uses: Swatinem/rust-cache@v2 - name: Setup mdBook uses: peaceiris/actions-mdbook@v1 diff --git a/.github/workflows/grcov.yml b/.github/workflows/grcov.yml index 23287dc46..b4a61db40 100644 --- a/.github/workflows/grcov.yml +++ b/.github/workflows/grcov.yml @@ -41,6 +41,7 @@ jobs: steps: - name: Checkout sources uses: actions/checkout@v4 + - uses: Swatinem/rust-cache@v2 - name: Setup database run: | From 48164d76fef229b8a472b07c7ffeabb100df7c7d Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Sat, 30 Sep 2023 00:45:15 -0400 Subject: [PATCH 012/108] CI: Combine cross docker builds (#910) Minor CI cleanup --- .github/workflows/ci.yml | 342 ++++++++++++++++++--------------------- 1 file changed, 161 insertions(+), 181 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e11fef128..ed5715f4a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,6 +24,161 @@ defaults: shell: bash jobs: + docker: + name: Build and test docker images + runs-on: ubuntu-latest + env: + # PG_* variables are used by psql + PGDATABASE: test + PGHOST: localhost + PGUSER: postgres + PGPASSWORD: postgres + TARGETS: "aarch64-unknown-linux-musl x86_64-unknown-linux-musl" + # TODO: aarch64-unknown-linux-gnu + services: + postgres: + image: postgis/postgis:15-3.3 + ports: + # will assign a random free host port + - 5432/tcp + # Sadly there is currently no way to pass arguments to the service image other than this hack + # See also https://stackoverflow.com/a/62720566/177275 + options: >- + -e POSTGRES_DB=test + -e POSTGRES_USER=postgres + -e POSTGRES_PASSWORD=postgres + -e PGDATABASE=test + -e PGUSER=postgres + -e PGPASSWORD=postgres + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + --entrypoint sh + postgis/postgis:15-3.3 + -c "exec docker-entrypoint.sh postgres -c ssl=on -c ssl_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem -c ssl_key_file=/etc/ssl/private/ssl-cert-snakeoil.key" + steps: + - name: Checkout sources + uses: actions/checkout@v4 + - uses: Swatinem/rust-cache@v2 + - name: Install cross + run: | + cargo install cross + # Install latest cross version from git (disabled as it is probably less stable) + # cargo install cross --git https://github.com/cross-rs/cross + cross --version + - name: Setup database + run: tests/fixtures/initdb.sh + env: + PGPORT: ${{ job.services.postgres.ports[5432] }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + # https://github.com/docker/setup-qemu-action + with: + platforms: linux/amd64,linux/arm64 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + # https://github.com/docker/setup-buildx-action + with: + install: true + platforms: linux/amd64,linux/arm64 + + - name: Build targets + run: | + for target in $TARGETS; do + echo -e "\n----------------------------------------------" + echo "Building $target" + + export "CARGO_TARGET_$(echo $target | tr 'a-z-' 'A-Z_')_RUSTFLAGS"='-C strip=debuginfo' + cross build --release --target $target --package martin-mbtiles + cross build --release --target $target --package martin --features=vendored-openssl + + mkdir -p target_releases/$target + mv target/$target/release/mbtiles target_releases/$target + mv target/$target/release/martin target_releases/$target + done + + - name: Save build artifacts to build-${{ matrix.target }} + uses: actions/upload-artifact@v3 + with: + name: cross-build + path: target_releases/* + - name: Reorganize artifacts for docker build + run: | + mkdir -p target_releases/linux/arm64 + mv target_releases/aarch64-unknown-linux-musl/* target_releases/linux/arm64/ + mkdir -p target_releases/linux/amd64 + mv target_releases/x86_64-unknown-linux-musl/* target_releases/linux/amd64/ + + - name: Build linux/arm64 Docker image + uses: docker/build-push-action@v5 + # https://github.com/docker/build-push-action + with: + context: . + file: multi-platform.Dockerfile + load: true + tags: ${{ github.repository }}:linux-arm64 + platforms: linux/arm64 + - name: Test linux/arm64 Docker image + run: | + PLATFORM=linux/arm64 + TAG=${{ github.repository }}:linux-arm64 + export MBTILES_BUILD=- + export MBTILES_BIN="docker run --rm --net host --platform $PLATFORM -e DATABASE_URL -v $PWD/tests:/tests --entrypoint /usr/local/bin/mbtiles $TAG" + export MARTIN_BUILD=- + export MARTIN_BIN="docker run --rm --net host --platform $PLATFORM -e DATABASE_URL -v $PWD/tests:/tests $TAG" + tests/test.sh + env: + DATABASE_URL: postgres://${{ env.PGUSER }}:${{ env.PGUSER }}@${{ env.PGHOST }}:${{ job.services.postgres.ports[5432] }}/${{ env.PGDATABASE }}?sslmode=require + + - name: Build linux/amd64 Docker image + uses: docker/build-push-action@v5 + # https://github.com/docker/build-push-action + with: + context: . + file: multi-platform.Dockerfile + load: true + tags: ${{ github.repository }}:linux-amd64 + platforms: linux/amd64 + - name: Test linux/amd64 Docker image + run: | + PLATFORM=linux/amd64 + TAG=${{ github.repository }}:linux-amd64 + export MBTILES_BUILD=- + export MBTILES_BIN="docker run --rm --net host --platform $PLATFORM -e DATABASE_URL -v $PWD/tests:/tests --entrypoint /usr/local/bin/mbtiles $TAG" + export MARTIN_BUILD=- + export MARTIN_BIN="docker run --rm --net host --platform $PLATFORM -e DATABASE_URL -v $PWD/tests:/tests $TAG" + tests/test.sh + env: + DATABASE_URL: postgres://${{ env.PGUSER }}:${{ env.PGUSER }}@${{ env.PGHOST }}:${{ job.services.postgres.ports[5432] }}/${{ env.PGDATABASE }}?sslmode=require + + - name: Login to GitHub Docker registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + # https://github.com/docker/login-action + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Docker meta + id: docker_meta + uses: docker/metadata-action@v5 + # https://github.com/docker/metadata-action + with: + images: ghcr.io/${{ github.repository }} + - name: Push the Docker image + if: github.event_name != 'pull_request' + uses: docker/build-push-action@v5 + with: + context: . + file: multi-platform.Dockerfile + push: true + tags: ${{ steps.docker_meta.outputs.tags }} + labels: ${{ steps.docker_meta.outputs.labels }} + platforms: linux/amd64,linux/arm64 + build: name: Build ${{ matrix.target }} runs-on: ${{ matrix.os }} @@ -99,42 +254,6 @@ jobs: name: build-${{ matrix.target }} path: target_releases/* - build-cross: - name: Cross-platform builds - runs-on: ubuntu-latest - env: - TARGETS: "aarch64-unknown-linux-musl x86_64-unknown-linux-musl" - # TODO: aarch64-unknown-linux-gnu - steps: - - name: Checkout sources - uses: actions/checkout@v4 - - uses: Swatinem/rust-cache@v2 - - name: Install cross - run: | - cargo install cross - # Install latest cross version from git (disabled as it is probably less stable) - # cargo install cross --git https://github.com/cross-rs/cross - cross --version - - name: Build targets - run: | - for target in $TARGETS; do - echo -e "\n----------------------------------------------" - echo "Building $target" - - export "CARGO_TARGET_$(echo $target | tr 'a-z-' 'A-Z_')_RUSTFLAGS"='-C strip=debuginfo' - cross build --release --target $target --package martin-mbtiles - cross build --release --target $target --package martin --features=vendored-openssl - - mkdir -p target_releases/$target - mv target/$target/release/mbtiles target_releases/$target - mv target/$target/release/martin target_releases/$target - done - - name: Save build artifacts to build-${{ matrix.target }} - uses: actions/upload-artifact@v3 - with: - name: build-cross - path: target_releases/* - test: name: Test ${{ matrix.target }} runs-on: ${{ matrix.os }} @@ -335,148 +454,10 @@ jobs: path: tests/output/* retention-days: 5 - docker: - name: Build docker images - runs-on: ubuntu-latest - needs: [ build, build-cross ] - env: - # PG_* variables are used by psql - PGDATABASE: test - PGHOST: localhost - PGUSER: postgres - PGPASSWORD: postgres - services: - postgres: - image: postgis/postgis:15-3.3 - ports: - # will assign a random free host port - - 5432/tcp - # Sadly there is currently no way to pass arguments to the service image other than this hack - # See also https://stackoverflow.com/a/62720566/177275 - options: >- - -e POSTGRES_DB=test - -e POSTGRES_USER=postgres - -e POSTGRES_PASSWORD=postgres - -e PGDATABASE=test - -e PGUSER=postgres - -e PGPASSWORD=postgres - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - --entrypoint sh - postgis/postgis:15-3.3 - -c "exec docker-entrypoint.sh postgres -c ssl=on -c ssl_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem -c ssl_key_file=/etc/ssl/private/ssl-cert-snakeoil.key" - steps: - - name: Checkout sources - uses: actions/checkout@v4 - - uses: Swatinem/rust-cache@v2 - - name: Setup database - run: tests/fixtures/initdb.sh - env: - PGPORT: ${{ job.services.postgres.ports[5432] }} - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - # https://github.com/docker/setup-qemu-action - with: - platforms: linux/amd64,linux/arm64 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - # https://github.com/docker/setup-buildx-action - with: - install: true - platforms: linux/amd64,linux/arm64 - - - run: rm -rf target_releases2 - - name: Download build-cross artifacts - uses: actions/download-artifact@v3 - with: - name: build-cross - path: target_releases2 - - name: Reorganize build artifacts - run: | - chmod -R +x target_releases2/ - mkdir -p target_releases/linux/arm64 - mv target_releases2/aarch64-unknown-linux-musl/* target_releases/linux/arm64/ - mkdir -p target_releases/linux/amd64 - mv target_releases2/x86_64-unknown-linux-musl/* target_releases/linux/amd64/ - rm -rf target_releases2 - - - name: Build linux/arm64 Docker image - id: docker_aarch64-unknown-linux-gnu - uses: docker/build-push-action@v5 - # https://github.com/docker/build-push-action - with: - context: . - file: multi-platform.Dockerfile - load: true - tags: ${{ github.repository }}:linux-arm64 - platforms: linux/arm64 - - name: Test linux/arm64 Docker image - run: | - PLATFORM=linux/arm64 - TAG=${{ github.repository }}:linux-arm64 - export MBTILES_BUILD=- - export MBTILES_BIN="docker run --rm --net host --platform $PLATFORM -e DATABASE_URL -v $PWD/tests:/tests --entrypoint /usr/local/bin/mbtiles $TAG" - export MARTIN_BUILD=- - export MARTIN_BIN="docker run --rm --net host --platform $PLATFORM -e DATABASE_URL -v $PWD/tests:/tests $TAG" - tests/test.sh - env: - DATABASE_URL: postgres://${{ env.PGUSER }}:${{ env.PGUSER }}@${{ env.PGHOST }}:${{ job.services.postgres.ports[5432] }}/${{ env.PGDATABASE }}?sslmode=require - - - name: Build linux/amd64 Docker image - id: docker_x86_64-unknown-linux-gnu - uses: docker/build-push-action@v5 - # https://github.com/docker/build-push-action - with: - context: . - file: multi-platform.Dockerfile - load: true - tags: ${{ github.repository }}:linux-amd64 - platforms: linux/amd64 - - name: Test linux/amd64 Docker image - run: | - PLATFORM=linux/amd64 - TAG=${{ github.repository }}:linux-amd64 - export MBTILES_BUILD=- - export MBTILES_BIN="docker run --rm --net host --platform $PLATFORM -e DATABASE_URL -v $PWD/tests:/tests --entrypoint /usr/local/bin/mbtiles $TAG" - export MARTIN_BUILD=- - export MARTIN_BIN="docker run --rm --net host --platform $PLATFORM -e DATABASE_URL -v $PWD/tests:/tests $TAG" - tests/test.sh - env: - DATABASE_URL: postgres://${{ env.PGUSER }}:${{ env.PGUSER }}@${{ env.PGHOST }}:${{ job.services.postgres.ports[5432] }}/${{ env.PGDATABASE }}?sslmode=require - - - name: Login to GitHub Docker registry - if: github.event_name != 'pull_request' - uses: docker/login-action@v3 - # https://github.com/docker/login-action - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Docker meta - id: docker_meta - uses: docker/metadata-action@v5 - # https://github.com/docker/metadata-action - with: - images: ghcr.io/${{ github.repository }} - - name: Push the Docker image - if: github.event_name != 'pull_request' - uses: docker/build-push-action@v5 - with: - context: . - file: multi-platform.Dockerfile - push: true - tags: ${{ steps.docker_meta.outputs.tags }} - labels: ${{ steps.docker_meta.outputs.labels }} - platforms: linux/amd64,linux/arm64 - package: name: Package ${{ matrix.target }} runs-on: ${{ matrix.os }} - needs: [ test, test-legacy ] + needs: [ docker, test, test-legacy ] strategy: fail-fast: true matrix: @@ -488,9 +469,6 @@ jobs: - target: debian-x86_64 os: ubuntu-latest name: martin-Debian-x86_64.deb - # - target: aarch64-unknown-linux-gnu - # os: ubuntu-latest - # name: martin-Linux-aarch64.tar.gz - target: x86_64-apple-darwin os: macOS-latest name: martin-Darwin-x86_64.tar.gz @@ -502,7 +480,9 @@ jobs: - target: x86_64-unknown-linux-gnu os: ubuntu-latest name: martin-Linux-x86_64.tar.gz - # From the cross build + # + # From the cross-build + # - target: aarch64-unknown-linux-musl os: ubuntu-latest cross: 'true' @@ -524,7 +504,7 @@ jobs: if: matrix.cross == 'true' uses: actions/download-artifact@v3 with: - name: build-cross + name: cross-build path: target/ - name: Package run: | @@ -558,7 +538,7 @@ jobs: done: name: CI Finished runs-on: ubuntu-latest - needs: [ docker, package ] + needs: [ package ] steps: - name: Finished run: echo "CI finished successfully" From 31acc7fd3ced82136282f783c975db5c4c2ac674 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Sat, 30 Sep 2023 00:52:08 -0400 Subject: [PATCH 013/108] Do not use rust-cache on release --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ed5715f4a..33e83662f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,6 +61,7 @@ jobs: - name: Checkout sources uses: actions/checkout@v4 - uses: Swatinem/rust-cache@v2 + if: github.event_name != 'release' && github.event_name != 'workflow_dispatch' - name: Install cross run: | cargo install cross @@ -205,6 +206,7 @@ jobs: - name: Rust Versions run: rustc --version && cargo --version - uses: Swatinem/rust-cache@v2 + if: github.event_name != 'release' && github.event_name != 'workflow_dispatch' - name: Lint (Linux) if: matrix.target == 'x86_64-unknown-linux-gnu' run: | @@ -273,6 +275,7 @@ jobs: - name: Checkout sources uses: actions/checkout@v4 - uses: Swatinem/rust-cache@v2 + if: github.event_name != 'release' && github.event_name != 'workflow_dispatch' - name: Start postgres uses: nyurik/action-setup-postgis@v1 id: pg @@ -399,6 +402,7 @@ jobs: - name: Checkout sources uses: actions/checkout@v4 - uses: Swatinem/rust-cache@v2 + if: github.event_name != 'release' && github.event_name != 'workflow_dispatch' - name: Setup database run: tests/fixtures/initdb.sh env: From 8d7204c53de242165a433afdb7bfc6f20dcce19b Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Sat, 30 Sep 2023 05:58:49 -0400 Subject: [PATCH 014/108] Switch to Rustls (#474) Fixes #471 --- .github/workflows/ci.yml | 20 +--- Cargo.lock | 240 +++++++++++++++++++++++-------------- Cargo.toml | 6 +- Dockerfile | 4 +- arm64.Dockerfile | 3 +- docs/src/development.md | 4 +- docs/src/installation.md | 2 - homebrew-formula/martin.rb | 1 + justfile | 1 + martin/Cargo.toml | 11 +- martin/src/args/pg.rs | 13 -- martin/src/pg/config.rs | 9 +- martin/src/pg/errors.rs | 30 +++-- martin/src/pg/pool.rs | 56 ++++++--- martin/src/pg/tls.rs | 124 ++++++++++++------- tests/test.sh | 4 +- 16 files changed, 312 insertions(+), 216 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 33e83662f..d67a1d464 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -93,7 +93,7 @@ jobs: export "CARGO_TARGET_$(echo $target | tr 'a-z-' 'A-Z_')_RUSTFLAGS"='-C strip=debuginfo' cross build --release --target $target --package martin-mbtiles - cross build --release --target $target --package martin --features=vendored-openssl + cross build --release --target $target --package martin mkdir -p target_releases/$target mv target/$target/release/mbtiles target_releases/$target @@ -216,14 +216,7 @@ jobs: cargo clippy --package martin-mbtiles --no-default-features -- -D warnings cargo clippy --package martin-mbtiles -- -D warnings cargo clippy --package martin -- -D warnings - cargo clippy --package martin --features vendored-openssl -- -D warnings cargo clippy --package martin --features bless-tests -- -D warnings - - name: Install OpenSSL (Windows) - if: runner.os == 'Windows' - shell: powershell - run: | - echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append - vcpkg install openssl:x64-windows-static-md - name: Build (.deb) if: matrix.target == 'debian-x86_64' run: | @@ -235,17 +228,11 @@ jobs: - name: Build if: matrix.target != 'debian-x86_64' run: | - rustup target add "${{ matrix.target }}" - if [[ "${{ runner.os }}" == "Windows" ]]; then - FEATURES="ssl" - else - FEATURES="vendored-openssl" - fi - set -x + rustup target add "${{ matrix.target }}" export RUSTFLAGS='-C strip=debuginfo' cargo build --release --target ${{ matrix.target }} --package martin-mbtiles - cargo build --release --target ${{ matrix.target }} --package martin --features=$FEATURES + cargo build --release --target ${{ matrix.target }} --package martin mkdir -p target_releases mv target/${{ matrix.target }}/release/mbtiles${{ matrix.ext }} target_releases/ @@ -300,7 +287,6 @@ jobs: cargo test --package martin-mbtiles --no-default-features cargo test --package martin-mbtiles cargo test --package martin - cargo test --package martin --features vendored-openssl cargo test --doc RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --workspace cargo clean diff --git a/Cargo.lock b/Cargo.lock index a2373b4b9..77103cd50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -687,6 +687,22 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + [[package]] name = "cpufeatures" version = "0.2.9" @@ -1147,21 +1163,6 @@ dependencies = [ "ttf-parser 0.19.2", ] -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "form_urlencoded" version = "1.2.0" @@ -1758,13 +1759,14 @@ dependencies = [ "martin-mbtiles", "martin-tile-utils", "num_cpus", - "openssl", "pmtiles", "postgis", "postgres", - "postgres-openssl", "postgres-protocol", "regex", + "rustls", + "rustls-native-certs", + "rustls-pemfile", "semver", "serde", "serde_json", @@ -1774,6 +1776,7 @@ dependencies = [ "thiserror", "tilejson", "tokio", + "tokio-postgres-rustls", ] [[package]] @@ -1988,52 +1991,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" [[package]] -name = "openssl" -version = "0.10.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c" -dependencies = [ - "bitflags 2.4.0", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.37", -] - -[[package]] -name = "openssl-src" -version = "300.1.5+3.1.3" +name = "openssl-probe" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "559068e4c12950d7dcaa1857a61725c0d38d4fc03ff8e070ab31a75d6e316491" -dependencies = [ - "cc", -] - -[[package]] -name = "openssl-sys" -version = "0.9.93" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d" -dependencies = [ - "cc", - "libc", - "openssl-src", - "pkg-config", - "vcpkg", -] +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "oxipng" @@ -2275,19 +2236,6 @@ dependencies = [ "tokio-postgres", ] -[[package]] -name = "postgres-openssl" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1de0ea6504e07ca78355a6fb88ad0f36cafe9e696cbc6717f16a207f3a60be72" -dependencies = [ - "futures", - "openssl", - "tokio", - "tokio-openssl", - "tokio-postgres", -] - [[package]] name = "postgres-protocol" version = "0.6.6" @@ -2499,6 +2447,21 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin 0.5.2", + "untrusted", + "web-sys", + "winapi", +] + [[package]] name = "roxmltree" version = "0.18.0" @@ -2578,6 +2541,49 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "rustls" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +dependencies = [ + "base64", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "rustybuzz" version = "0.7.0" @@ -2609,12 +2615,54 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +dependencies = [ + "windows-sys", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.19" @@ -3340,18 +3388,6 @@ dependencies = [ "syn 2.0.37", ] -[[package]] -name = "tokio-openssl" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08f9ffb7809f1b20c1b398d92acf4cc719874b3b2b2d9ea2f09b4a80350878a" -dependencies = [ - "futures-util", - "openssl", - "openssl-sys", - "tokio", -] - [[package]] name = "tokio-postgres" version = "0.7.10" @@ -3378,6 +3414,30 @@ dependencies = [ "whoami", ] +[[package]] +name = "tokio-postgres-rustls" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5831152cb0d3f79ef5523b357319ba154795d64c7078b2daa95a803b54057f" +dependencies = [ + "futures", + "ring", + "rustls", + "tokio", + "tokio-postgres", + "tokio-rustls", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.14" @@ -3544,6 +3604,12 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa" +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "url" version = "2.4.1" diff --git a/Cargo.toml b/Cargo.toml index 9cc4e70e8..9893e655d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,13 +34,14 @@ log = "0.4" martin-mbtiles = { path = "./martin-mbtiles", version = "0.5.0", default-features = false } martin-tile-utils = { path = "./martin-tile-utils", version = "0.1.0" } num_cpus = "1" -openssl = "0.10" pmtiles = { version = "0.3", features = ["mmap-async-tokio", "tilejson"] } postgis = "0.9" postgres = { version = "0.19", features = ["with-time-0_3", "with-uuid-1", "with-serde_json-1"] } -postgres-openssl = "0.5" postgres-protocol = "0.6" regex = "1" +rustls = { version = "0.21", features = ["dangerous_configuration"] } +rustls-native-certs = "0.6" +rustls-pemfile = "1" semver = "1" serde = { version = "1", features = ["derive"] } serde_json = "1" @@ -52,6 +53,7 @@ subst = { version = "0.3", features = ["yaml"] } thiserror = "1" tilejson = "0.3" tokio = { version = "1.32.0", features = ["macros"] } +tokio-postgres-rustls = "0.10" [profile.dev.package.sqlx-macros] # See https://github.com/launchbadge/sqlx#compile-time-verification diff --git a/Dockerfile b/Dockerfile index 71ffdf5e8..035f84ab6 100755 --- a/Dockerfile +++ b/Dockerfile @@ -3,10 +3,10 @@ FROM rust:alpine as builder WORKDIR /usr/src/martin RUN apk update \ - && apk add --no-cache openssl-dev musl-dev perl build-base + && apk add --no-cache musl-dev perl build-base COPY . . -RUN CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse cargo build --release --features=vendored-openssl +RUN CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse cargo build --release FROM alpine:latest diff --git a/arm64.Dockerfile b/arm64.Dockerfile index 4912078d2..e15c42712 100644 --- a/arm64.Dockerfile +++ b/arm64.Dockerfile @@ -4,12 +4,11 @@ WORKDIR /usr/src/martin RUN apt-get update \ && apt-get install -y --no-install-recommends \ - libssl-dev \ perl \ && rm -rf /var/lib/apt/lists/* COPY . . -RUN CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse cargo build --release --features=vendored-openssl +RUN CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse cargo build --release FROM debian:bullseye-slim diff --git a/docs/src/development.md b/docs/src/development.md index adf969d09..86891fbed 100644 --- a/docs/src/development.md +++ b/docs/src/development.md @@ -20,11 +20,11 @@ Install [docker](https://docs.docker.com/get-docker/) and [docker-compose](https sudo apt install -y docker.io docker-compose ``` -Install a few libs and tools like [openssl](https://www.openssl.org/): +Install a few required libs and tools: ```shell, ignore # For Ubuntu-based distros -sudo apt install -y libssl-dev build-essential pkg-config jq file +sudo apt install -y build-essential pkg-config jq file ``` Install [Just](https://github.com/casey/just#readme) (improved makefile processor). Note that some Linux and Homebrew distros have outdated versions of Just, so you should install it from source: diff --git a/docs/src/installation.md b/docs/src/installation.md index f1bf68969..908d98925 100644 --- a/docs/src/installation.md +++ b/docs/src/installation.md @@ -25,8 +25,6 @@ cargo install martin martin --help ``` -If your PostgreSQL connection requires SSL, you would need to install OpenSSL and run `cargo install martin --features ssl`, or even install with `--features vendored-openssl` to [statically link OpenSSL](https://docs.rs/openssl/latest/openssl/#vendored) into the binary. - ## Homebrew If you are using macOS and [Homebrew](https://brew.sh/) you can install martin using Homebrew tap. diff --git a/homebrew-formula/martin.rb b/homebrew-formula/martin.rb index e89f08b46..006ba31a3 100644 --- a/homebrew-formula/martin.rb +++ b/homebrew-formula/martin.rb @@ -10,6 +10,7 @@ class Martin < Formula sha256 "92f660b1bef3a54dc84e4794a5ba02a8817c25f21ce7000783749bbae9e50de1" version "#{current_version}" + # FIXME: remove this for the 0.9 version depends_on "openssl@3" def install diff --git a/justfile b/justfile index a8aacc642..696b323a8 100644 --- a/justfile +++ b/justfile @@ -205,6 +205,7 @@ fmt2: # Run cargo clippy clippy: cargo clippy --workspace --all-targets --bins --tests --lib --benches -- -D warnings + RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --workspace # These steps automatically run before git push via a git hook [private] diff --git a/martin/Cargo.toml b/martin/Cargo.toml index 6eb70d11e..c3ea8cb73 100644 --- a/martin/Cargo.toml +++ b/martin/Cargo.toml @@ -24,7 +24,6 @@ revision = "" maintainer = "Yuri Astrakhan , Stepan Kuzmin , MapLibre contributors" maintainer-scripts = "../debian" depends = "$auto" -features = ["ssl"] assets = [ ["target/release/martin", "/usr/bin/martin", "755"], ["target/release/mbtiles", "/usr/bin/mbtiles", "755"], @@ -50,8 +49,6 @@ path = "src/bin/main.rs" [features] default = [] -vendored-openssl = ["ssl", "openssl?/vendored"] -ssl = ["dep:openssl", "dep:postgres-openssl"] bless-tests = [] [dependencies] @@ -78,6 +75,9 @@ postgis.workspace = true postgres-protocol.workspace = true postgres.workspace = true regex.workspace = true +rustls-native-certs.workspace = true +rustls-pemfile.workspace = true +rustls.workspace = true semver.workspace = true serde.workspace = true serde_json = { workspace = true, features = ["preserve_order"] } @@ -87,10 +87,7 @@ subst.workspace = true thiserror.workspace = true tilejson.workspace = true tokio = { workspace = true, features = ["io-std"] } - -# Optional dependencies for ssl support -openssl = { workspace = true, optional = true } -postgres-openssl = { workspace = true, optional = true } +tokio-postgres-rustls.workspace = true [dev-dependencies] cargo-husky.workspace = true diff --git a/martin/src/args/pg.rs b/martin/src/args/pg.rs index 88c9a1f86..f77c06716 100644 --- a/martin/src/args/pg.rs +++ b/martin/src/args/pg.rs @@ -13,7 +13,6 @@ pub struct PgArgs { #[arg(short = 'b', long)] pub disable_bounds: bool, /// Loads trusted root certificates from a file. The file should contain a sequence of PEM-formatted CA certificates. - #[cfg(feature = "ssl")] #[arg(long)] pub ca_root_file: Option, /// If a spatial PG table has SRID 0, then this default SRID will be used as a fallback. @@ -82,7 +81,6 @@ impl PgArgs { }); } - #[cfg(feature = "ssl")] if self.ca_root_file.is_some() { info!("Overriding root certificate file to {} on all Postgres connections because of a CLI parameter", self.ca_root_file.as_ref().unwrap().display()); @@ -145,13 +143,6 @@ impl PgArgs { }) } - #[cfg(not(feature = "ssl"))] - #[allow(clippy::unused_self)] - fn get_certs<'a>(&self, _env: &impl Env<'a>) -> PgSslCerts { - PgSslCerts {} - } - - #[cfg(feature = "ssl")] fn get_certs<'a>(&self, env: &impl Env<'a>) -> PgSslCerts { let mut result = PgSslCerts { ssl_cert: Self::parse_env_var(env, "PGSSLCERT", "ssl certificate"), @@ -172,7 +163,6 @@ impl PgArgs { result } - #[cfg(feature = "ssl")] fn parse_env_var<'a>( env: &impl Env<'a>, env_var: &str, @@ -194,7 +184,6 @@ fn is_postgresql_string(s: &str) -> bool { #[cfg(test)] mod tests { - #[cfg(feature = "ssl")] use std::path::PathBuf; use super::*; @@ -262,7 +251,6 @@ mod tests { Some(OneOrMany::One(PgConfig { connection_string: some("postgres://localhost:5432"), default_srid: Some(10), - #[cfg(feature = "ssl")] ssl_certificates: PgSslCerts { ssl_root_cert: Some(PathBuf::from("file")), ..Default::default() @@ -297,7 +285,6 @@ mod tests { Some(OneOrMany::One(PgConfig { connection_string: some("postgres://localhost:5432"), default_srid: Some(20), - #[cfg(feature = "ssl")] ssl_certificates: PgSslCerts { ssl_cert: Some(PathBuf::from("cert")), ssl_key: Some(PathBuf::from("key")), diff --git a/martin/src/pg/config.rs b/martin/src/pg/config.rs index 8d4207538..c84aa01b3 100644 --- a/martin/src/pg/config.rs +++ b/martin/src/pg/config.rs @@ -18,18 +18,15 @@ pub trait PgInfo { #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] pub struct PgSslCerts { /// Same as PGSSLCERT - /// https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLCERT - #[cfg(feature = "ssl")] + /// ([docs](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLCERT)) #[serde(skip_serializing_if = "Option::is_none")] pub ssl_cert: Option, /// Same as PGSSLKEY - /// https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLKEY - #[cfg(feature = "ssl")] + /// ([docs](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLKEY)) #[serde(skip_serializing_if = "Option::is_none")] pub ssl_key: Option, /// Same as PGSSLROOTCERT - /// https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLROOTCERT - #[cfg(feature = "ssl")] + /// ([docs](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-SSLROOTCERT)) #[serde(skip_serializing_if = "Option::is_none")] pub ssl_root_cert: Option, } diff --git a/martin/src/pg/errors.rs b/martin/src/pg/errors.rs index 62ddec726..3bf537b5e 100644 --- a/martin/src/pg/errors.rs +++ b/martin/src/pg/errors.rs @@ -1,3 +1,6 @@ +use std::io; +use std::path::PathBuf; + use deadpool_postgres::tokio_postgres::Error as TokioPgError; use deadpool_postgres::{BuildError, PoolError}; use semver::Version; @@ -10,23 +13,24 @@ pub type Result = std::result::Result; #[derive(thiserror::Error, Debug)] pub enum PgError { - #[cfg(feature = "ssl")] - #[error("Can't build TLS connection: {0}")] - BuildSslConnectorError(#[from] openssl::error::ErrorStack), + #[error("Cannot load platform root certificates: {0}")] + CannotLoadRoots(#[source] io::Error), + + #[error("Cannot open certificate file {}: {0}", .1.display())] + CannotOpenCert(#[source] io::Error, PathBuf), + + #[error("Cannot parse certificate file {}: {0}", .1.display())] + CannotParseCert(#[source] io::Error, PathBuf), - #[cfg(feature = "ssl")] - #[error("Can't set trusted root certificate {}: {0}", .1.display())] - BadTrustedRootCertError(#[source] openssl::error::ErrorStack, std::path::PathBuf), + #[error("Unable to parse PEM RSA key file {}", .0.display())] + InvalidPrivateKey(PathBuf), - #[cfg(feature = "ssl")] - #[error("Can't set client certificate {}: {0}", .1.display())] - BadClientCertError(#[source] openssl::error::ErrorStack, std::path::PathBuf), + #[error("Unable to use client certificate pair {} / {}: {0}", .1.display(), .2.display())] + CannotUseClientKey(#[source] rustls::Error, PathBuf, PathBuf), - #[cfg(feature = "ssl")] - #[error("Can't set client certificate key {}: {0}", .1.display())] - BadClientKeyError(#[source] openssl::error::ErrorStack, std::path::PathBuf), + #[error("Rustls Error: {0:?}")] + RustlsError(#[from] rustls::Error), - #[cfg(feature = "ssl")] #[error("Unknown SSL mode: {0:?}")] UnknownSslMode(deadpool_postgres::tokio_postgres::config::SslMode), diff --git a/martin/src/pg/pool.rs b/martin/src/pg/pool.rs index 15b26b9d9..ba3cb0b38 100755 --- a/martin/src/pg/pool.rs +++ b/martin/src/pg/pool.rs @@ -1,5 +1,6 @@ use deadpool_postgres::{Manager, ManagerConfig, Object, Pool, RecyclingMethod}; use log::{info, warn}; +use postgres::config::SslMode; use semver::Version; use crate::pg::config::PgConfig; @@ -27,25 +28,8 @@ pub struct PgPool { impl PgPool { pub async fn new(config: &PgConfig) -> Result { - let conn_str = config.connection_string.as_ref().unwrap().as_str(); - let (pg_cfg, ssl_mode) = parse_conn_str(conn_str)?; - if matches!(ssl_mode, SslModeOverride::Unmodified(_)) { - info!("Connecting to {pg_cfg:?}"); - } else { - info!("Connecting to {pg_cfg:?} with ssl_mode={ssl_mode:?}"); - } + let (id, mgr) = Self::parse_config(config)?; - let id = pg_cfg.get_dbname().map_or_else( - || format!("{:?}", pg_cfg.get_hosts()[0]), - ToString::to_string, - ); - - let connector = make_connector(&config.ssl_certificates, ssl_mode)?; - - let mgr_config = ManagerConfig { - recycling_method: RecyclingMethod::Fast, - }; - let mgr = Manager::from_config(pg_cfg, connector, mgr_config); let pool = Pool::builder(mgr) .max_size(config.pool_size.unwrap_or(POOL_SIZE_DEFAULT)) .build() @@ -80,6 +64,42 @@ SELECT Ok(Self { id, pool, margin }) } + fn parse_config(config: &PgConfig) -> Result<(String, Manager)> { + let conn_str = config.connection_string.as_ref().unwrap().as_str(); + let (pg_cfg, ssl_mode) = parse_conn_str(conn_str)?; + + let id = pg_cfg.get_dbname().map_or_else( + || format!("{:?}", pg_cfg.get_hosts()[0]), + ToString::to_string, + ); + + let mgr_config = ManagerConfig { + recycling_method: RecyclingMethod::Fast, + }; + + let mgr = if pg_cfg.get_ssl_mode() == SslMode::Disable { + info!("Connecting without SSL support: {pg_cfg:?}"); + let connector = deadpool_postgres::tokio_postgres::NoTls {}; + Manager::from_config(pg_cfg, connector, mgr_config) + } else { + match ssl_mode { + SslModeOverride::Unmodified(_) => { + info!("Connecting with SSL support: {pg_cfg:?}"); + } + SslModeOverride::VerifyCa => { + info!("Using sslmode=verify-ca to connect: {pg_cfg:?}"); + } + SslModeOverride::VerifyFull => { + info!("Using sslmode=verify-full to connect: {pg_cfg:?}"); + } + }; + let connector = make_connector(&config.ssl_certificates, ssl_mode)?; + Manager::from_config(pg_cfg, connector, mgr_config) + }; + + Ok((id, mgr)) + } + pub async fn get(&self) -> Result { get_conn(&self.pool, self.id.as_str()).await } diff --git a/martin/src/pg/tls.rs b/martin/src/pg/tls.rs index 06186fadd..f143b9a64 100644 --- a/martin/src/pg/tls.rs +++ b/martin/src/pg/tls.rs @@ -1,20 +1,20 @@ +use std::fs::File; +use std::io::BufReader; +use std::path::PathBuf; use std::str::FromStr; use deadpool_postgres::tokio_postgres::config::SslMode; use deadpool_postgres::tokio_postgres::Config; -#[cfg(feature = "ssl")] use log::{info, warn}; -#[cfg(feature = "ssl")] -use openssl::ssl::SslFiletype; -#[cfg(feature = "ssl")] -use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; use regex::Regex; +use rustls::{Certificate, PrivateKey}; +use rustls_native_certs::load_native_certs; +use rustls_pemfile::Item::RSAKey; +use tokio_postgres_rustls::MakeRustlsConnect; -use crate::pg::PgError::BadConnectionString; -#[cfg(feature = "ssl")] use crate::pg::PgError::{ - BadClientCertError, BadClientKeyError, BadTrustedRootCertError, BuildSslConnectorError, - UnknownSslMode, + BadConnectionString, CannotLoadRoots, CannotOpenCert, CannotParseCert, CannotUseClientKey, + InvalidPrivateKey, UnknownSslMode, }; use crate::pg::{PgSslCerts, Result}; @@ -51,21 +51,41 @@ pub fn parse_conn_str(conn_str: &str) -> Result<(Config, SslModeOverride)> { Ok((pg_cfg, mode)) } -#[cfg(not(feature = "ssl"))] -#[allow(clippy::unnecessary_wraps)] -pub fn make_connector( - _pg_certs: &PgSslCerts, - _ssl_mode: SslModeOverride, -) -> Result { - Ok(deadpool_postgres::tokio_postgres::NoTls) +struct NoCertificateVerification {} + +impl rustls::client::ServerCertVerifier for NoCertificateVerification { + fn verify_server_cert( + &self, + _end_entity: &Certificate, + _intermediates: &[Certificate], + _server_name: &rustls::ServerName, + _scts: &mut dyn Iterator, + _ocsp: &[u8], + _now: std::time::SystemTime, + ) -> std::result::Result { + Ok(rustls::client::ServerCertVerified::assertion()) + } +} + +fn read_certs(file: &PathBuf) -> Result> { + Ok(rustls_pemfile::certs(&mut cert_reader(file)?) + .map_err(|e| CannotParseCert(e, file.clone()))? + .into_iter() + .map(Certificate) + .collect()) +} + +fn cert_reader(file: &PathBuf) -> Result> { + Ok(BufReader::new( + File::open(file).map_err(|e| CannotOpenCert(e, file.clone()))?, + )) } -#[cfg(feature = "ssl")] pub fn make_connector( pg_certs: &PgSslCerts, ssl_mode: SslModeOverride, -) -> Result { - let (verify_ca, verify_hostname) = match ssl_mode { +) -> Result { + let (verify_ca, _verify_hostname) = match ssl_mode { SslModeOverride::Unmodified(mode) => match mode { SslMode::Disable | SslMode::Prefer => (false, false), SslMode::Require => match pg_certs.ssl_root_cert { @@ -84,40 +104,58 @@ pub fn make_connector( SslModeOverride::VerifyFull => (true, true), }; - let tls = SslMethod::tls_client(); - let mut builder = SslConnector::builder(tls).map_err(BuildSslConnectorError)?; - - if let (Some(cert), Some(key)) = (&pg_certs.ssl_cert, &pg_certs.ssl_key) { - builder - .set_certificate_file(cert, SslFiletype::PEM) - .map_err(|e| BadClientCertError(e, cert.clone()))?; - builder - .set_private_key_file(key, SslFiletype::PEM) - .map_err(|e| BadClientKeyError(e, key.clone()))?; - } else if pg_certs.ssl_key.is_some() || pg_certs.ssl_key.is_some() { - warn!("SSL client certificate and key files must be set to use client certificate with Postgres. Only one of them was set."); - } + let mut roots = rustls::RootCertStore::empty(); if let Some(file) = &pg_certs.ssl_root_cert { - builder - .set_ca_file(file) - .map_err(|e| BadTrustedRootCertError(e, file.clone()))?; + for cert in read_certs(file)? { + roots.add(&cert)?; + } info!("Using {} as a root certificate", file.display()); } - if !verify_ca { - builder.set_verify(SslVerifyMode::NONE); + if verify_ca || pg_certs.ssl_root_cert.is_some() || pg_certs.ssl_cert.is_some() { + let certs = load_native_certs().map_err(CannotLoadRoots)?; + for cert in certs { + roots.add(&Certificate(cert.0))?; + } } - let mut connector = postgres_openssl::MakeTlsConnector::new(builder.build()); + let builder = rustls::ClientConfig::builder() + .with_safe_defaults() + .with_root_certificates(roots); + + let mut builder = if let (Some(cert), Some(key)) = (&pg_certs.ssl_cert, &pg_certs.ssl_key) { + match rustls_pemfile::read_one(&mut cert_reader(key)?) + .map_err(|e| CannotParseCert(e, key.clone()))? + { + Some(RSAKey(rsa_key)) => builder + .with_client_auth_cert(read_certs(cert)?, PrivateKey(rsa_key)) + .map_err(|e| CannotUseClientKey(e, cert.clone(), key.clone()))?, + _ => Err(InvalidPrivateKey(key.clone()))?, + } + } else { + if pg_certs.ssl_key.is_some() || pg_certs.ssl_key.is_some() { + warn!("SSL client certificate and key files must be set to use client certificate with Postgres. Only one of them was set."); + } + builder.with_no_client_auth() + }; - if !verify_hostname { - connector.set_callback(|cfg, _domain| { - cfg.set_verify_hostname(false); - Ok(()) - }); + if !verify_ca { + builder + .dangerous() + .set_certificate_verifier(std::sync::Arc::new(NoCertificateVerification {})); } + let connector = MakeRustlsConnect::new(builder); + + // TODO: ??? + // if !verify_hostname { + // connector.set_callback(|cfg, _domain| { + // cfg.set_verify_hostname(false); + // Ok(()) + // }); + // } + Ok(connector) } diff --git a/tests/test.sh b/tests/test.sh index ab47e712a..ca08a7b86 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -4,11 +4,11 @@ set -euo pipefail # TODO: use --fail-with-body to get the response body on failure CURL=${CURL:-curl --silent --show-error --fail --compressed} DATABASE_URL="${DATABASE_URL:-postgres://postgres@localhost/db}" -MARTIN_BUILD="${MARTIN_BUILD:-cargo build --features ssl}" +MARTIN_BUILD="${MARTIN_BUILD:-cargo build}" MARTIN_PORT="${MARTIN_PORT:-3111}" MARTIN_URL="http://localhost:${MARTIN_PORT}" MARTIN_ARGS="${MARTIN_ARGS:---listen-addresses localhost:${MARTIN_PORT}}" -MARTIN_BIN="${MARTIN_BIN:-cargo run --features ssl --} ${MARTIN_ARGS}" +MARTIN_BIN="${MARTIN_BIN:-cargo run --} ${MARTIN_ARGS}" MBTILES_BUILD="${MBTILES_BUILD:-cargo build -p martin-mbtiles}" MBTILES_BIN="${MBTILES_BIN:-target/debug/mbtiles}" From d1fe0266393c486feb0c0d6d1d4fcc1f355340de Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Sat, 30 Sep 2023 12:03:23 -0400 Subject: [PATCH 015/108] Fix justfile ssl test, print hba config --- justfile | 6 +++--- tests/fixtures/initdb.sh | 6 ++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/justfile b/justfile index 696b323a8..08799320e 100644 --- a/justfile +++ b/justfile @@ -81,14 +81,14 @@ bench-http: (cargo-install "oha") oha -z 120s http://localhost:3000/function_zxy_query/18/235085/122323 # Run all tests using a test database -test: (docker-up "db") test-unit test-int +test: start test-unit test-int # Run all tests using an SSL connection to a test database. Expected output won't match. -test-ssl: (docker-up "ssl") test-unit clean-test +test-ssl: start-ssl test-unit clean-test tests/test.sh # Run all tests using the oldest supported version of the database -test-legacy: (docker-up "db-legacy") test-unit test-int +test-legacy: start-legacy test-unit test-int # Run Rust unit and doc tests (cargo test) test-unit *ARGS: diff --git a/tests/fixtures/initdb.sh b/tests/fixtures/initdb.sh index e6c556f2a..20bb45615 100755 --- a/tests/fixtures/initdb.sh +++ b/tests/fixtures/initdb.sh @@ -33,3 +33,9 @@ echo "########################################################################## for sql_file in "$FIXTURES_DIR"/functions/*.sql; do psql -e -P pager=off -v ON_ERROR_STOP=1 -f "$sql_file" done + +echo -e "\n\n\n" +echo "################################################################################################" +echo "Active pg_hba.conf configuration" +echo "################################################################################################" +psql -P pager=off -v ON_ERROR_STOP=1 -c "select * from pg_hba_file_rules;" From ae8e0709d781f6617582be2f7754d41b437fd86d Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Sat, 30 Sep 2023 12:38:57 -0400 Subject: [PATCH 016/108] ssl dockercompose test config --- docker-compose.yml | 1 + tests/fixtures/initdb-dc-ssl.sh | 17 +++++++++++++++++ tests/fixtures/initdb.sh | 1 + 3 files changed, 19 insertions(+) create mode 100755 tests/fixtures/initdb-dc-ssl.sh diff --git a/docker-compose.yml b/docker-compose.yml index 1bab0390d..bda054125 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -72,6 +72,7 @@ services: - PGPASSWORD=postgres volumes: - ./tests/fixtures:/fixtures + - ./tests/fixtures/initdb-dc-ssl.sh:/docker-entrypoint-initdb.d/10_martin.sh - ./tests/fixtures/initdb-dc.sh:/docker-entrypoint-initdb.d/20_martin.sh db-legacy: diff --git a/tests/fixtures/initdb-dc-ssl.sh b/tests/fixtures/initdb-dc-ssl.sh new file mode 100755 index 000000000..f67675344 --- /dev/null +++ b/tests/fixtures/initdb-dc-ssl.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env sh +set -e + +mv /var/lib/postgresql/data/pg_hba.conf /var/lib/postgresql/data/pg_hba.conf.bak +cat > /var/lib/postgresql/data/pg_hba.conf < Date: Sat, 30 Sep 2023 22:49:56 -0400 Subject: [PATCH 017/108] Improve SSL mode testing (#913) --- .github/workflows/ci.yml | 142 ++++++++++++++++++--------- Cargo.lock | 28 +++--- docker-compose.yml | 28 ++++++ justfile | 29 +++++- tests/fixtures/initdb-dc-ssl-cert.sh | 18 ++++ tests/test.sh | 11 ++- 6 files changed, 191 insertions(+), 65 deletions(-) create mode 100755 tests/fixtures/initdb-dc-ssl-cert.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d67a1d464..e094397e8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,68 @@ defaults: shell: bash jobs: - docker: + lint-debug-test: + name: Lint and Unit test + runs-on: ubuntu-latest + env: + PGDATABASE: test + PGHOST: localhost + PGUSER: postgres + PGPASSWORD: postgres + services: + postgres: + image: postgis/postgis:16-3.4 + ports: + # will assign a random free host port + - 5432/tcp + # Sadly there is currently no way to pass arguments to the service image other than this hack + # See also https://stackoverflow.com/a/62720566/177275 + options: >- + -e POSTGRES_DB=test + -e POSTGRES_USER=postgres + -e POSTGRES_PASSWORD=postgres + -e PGDATABASE=test + -e PGUSER=postgres + -e PGPASSWORD=postgres + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + --entrypoint sh + postgis/postgis:16-3.4 + -c "exec docker-entrypoint.sh postgres -c ssl=on -c ssl_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem -c ssl_key_file=/etc/ssl/private/ssl-cert-snakeoil.key" + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Rust Versions + run: rustc --version && cargo --version + - uses: Swatinem/rust-cache@v2 + if: github.event_name != 'release' && github.event_name != 'workflow_dispatch' + - run: cargo fmt --all -- --check + - run: cargo clippy --package martin-tile-utils -- -D warnings + - run: cargo clippy --package martin-mbtiles --no-default-features -- -D warnings + - run: cargo clippy --package martin-mbtiles -- -D warnings + - run: cargo clippy --package martin -- -D warnings + - run: cargo clippy --package martin --features bless-tests -- -D warnings + - run: cargo doc --no-deps --workspace + env: + RUSTDOCFLAGS: "-D warnings" + - name: Init database + run: tests/fixtures/initdb.sh + env: + PGPORT: ${{ job.services.postgres.ports[5432] }} + - name: Run cargo test + run: | + set -x + cargo test --package martin-tile-utils + cargo test --package martin-mbtiles --no-default-features + cargo test --package martin-mbtiles + cargo test --package martin + cargo test --doc + env: + DATABASE_URL: postgres://${{ env.PGUSER }}:${{ env.PGUSER }}@${{ env.PGHOST }}:${{ job.services.postgres.ports[5432] }}/${{ env.PGDATABASE }}?sslmode=require + + docker-build-test: name: Build and test docker images runs-on: ubuntu-latest env: @@ -39,10 +100,7 @@ jobs: postgres: image: postgis/postgis:15-3.3 ports: - # will assign a random free host port - 5432/tcp - # Sadly there is currently no way to pass arguments to the service image other than this hack - # See also https://stackoverflow.com/a/62720566/177275 options: >- -e POSTGRES_DB=test -e POSTGRES_USER=postgres @@ -68,7 +126,7 @@ jobs: # Install latest cross version from git (disabled as it is probably less stable) # cargo install cross --git https://github.com/cross-rs/cross cross --version - - name: Setup database + - name: Init database run: tests/fixtures/initdb.sh env: PGPORT: ${{ job.services.postgres.ports[5432] }} @@ -207,19 +265,10 @@ jobs: run: rustc --version && cargo --version - uses: Swatinem/rust-cache@v2 if: github.event_name != 'release' && github.event_name != 'workflow_dispatch' - - name: Lint (Linux) - if: matrix.target == 'x86_64-unknown-linux-gnu' - run: | - set -x - cargo fmt --all -- --check - cargo clippy --package martin-tile-utils -- -D warnings - cargo clippy --package martin-mbtiles --no-default-features -- -D warnings - cargo clippy --package martin-mbtiles -- -D warnings - cargo clippy --package martin -- -D warnings - cargo clippy --package martin --features bless-tests -- -D warnings - name: Build (.deb) if: matrix.target == 'debian-x86_64' run: | + set -x sudo apt-get install -y dpkg dpkg-dev liblzma-dev cargo install cargo-deb cargo deb -v -p martin --output target/debian/debian-x86_64.deb @@ -233,7 +282,6 @@ jobs: export RUSTFLAGS='-C strip=debuginfo' cargo build --release --target ${{ matrix.target }} --package martin-mbtiles cargo build --release --target ${{ matrix.target }} --package martin - mkdir -p target_releases mv target/${{ matrix.target }}/release/mbtiles${{ matrix.ext }} target_releases/ mv target/${{ matrix.target }}/release/martin${{ matrix.ext }} target_releases/ @@ -243,8 +291,8 @@ jobs: name: build-${{ matrix.target }} path: target_releases/* - test: - name: Test ${{ matrix.target }} + test-multi-os: + name: Test on ${{ matrix.os }} runs-on: ${{ matrix.os }} needs: [ build ] strategy: @@ -279,19 +327,6 @@ jobs: tests/fixtures/initdb.sh env: DATABASE_URL: ${{ steps.pg.outputs.connection-uri }} - - name: Unit Tests (Linux) - if: matrix.target == 'x86_64-unknown-linux-gnu' - run: | - set -x - cargo test --package martin-tile-utils - cargo test --package martin-mbtiles --no-default-features - cargo test --package martin-mbtiles - cargo test --package martin - cargo test --doc - RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --workspace - cargo clean - env: - DATABASE_URL: ${{ steps.pg.outputs.connection-uri }} - name: Download build artifact build-${{ matrix.target }} uses: actions/download-artifact@v3 with: @@ -337,8 +372,8 @@ jobs: path: tests/output/* retention-days: 5 - test-legacy: - name: Test Legacy DB + test-with-svc: + name: Test postgis:${{ matrix.img_ver }} sslmode=${{ matrix.sslmode }} runs-on: ubuntu-latest needs: [ build ] strategy: @@ -346,30 +381,35 @@ jobs: matrix: include: # These must match the versions of postgres used in the docker-compose.yml - - image: postgis/postgis:11-3.0-alpine + - img_ver: 11-3.0-alpine args: postgres sslmode: disable - - image: postgis/postgis:14-3.3-alpine + - img_ver: 14-3.3-alpine args: postgres sslmode: disable # alpine images don't support SSL, so for this we use the debian images - - image: postgis/postgis:15-3.3 + - img_ver: 15-3.3 args: postgres -c ssl=on -c ssl_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem -c ssl_key_file=/etc/ssl/private/ssl-cert-snakeoil.key sslmode: require + # + # FIXME! + # DISABLED because Rustls fails to validate name (CN?) with the NotValidForName error + #- img_ver: 15-3.3 + # args: postgres -c ssl=on -c ssl_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem -c ssl_key_file=/etc/ssl/private/ssl-cert-snakeoil.key + # sslmode: verify-ca + #- img_ver: 15-3.3 + # args: postgres -c ssl=on -c ssl_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem -c ssl_key_file=/etc/ssl/private/ssl-cert-snakeoil.key + # sslmode: verify-full env: - # PG_* variables are used by psql PGDATABASE: test PGHOST: localhost PGUSER: postgres PGPASSWORD: postgres services: postgres: - image: ${{ matrix.image }} + image: postgis/postgis:${{ matrix.img_ver }} ports: - # will assign a random free host port - 5432/tcp - # Sadly there is currently no way to pass arguments to the service image other than this hack - # See also https://stackoverflow.com/a/62720566/177275 options: >- -e POSTGRES_DB=test -e POSTGRES_USER=postgres @@ -382,17 +422,24 @@ jobs: --health-timeout 5s --health-retries 5 --entrypoint sh - ${{ matrix.image }} + postgis/postgis:${{ matrix.img_ver }} -c "exec docker-entrypoint.sh ${{ matrix.args }}" steps: - name: Checkout sources uses: actions/checkout@v4 - uses: Swatinem/rust-cache@v2 if: github.event_name != 'release' && github.event_name != 'workflow_dispatch' - - name: Setup database + - name: Init database run: tests/fixtures/initdb.sh env: PGPORT: ${{ job.services.postgres.ports[5432] }} + - name: Get DB SSL cert (sslmode=verify-*) + if: matrix.sslmode == 'verify-ca' || matrix.sslmode == 'verify-full' + run: | + set -x + mkdir -p target/certs + docker cp ${{ job.services.postgres.id }}:/etc/ssl/certs/ssl-cert-snakeoil.pem target/certs/server.crt + docker cp ${{ job.services.postgres.id }}:/etc/ssl/private/ssl-cert-snakeoil.key target/certs/server.key - name: Download build artifact build-x86_64-unknown-linux-gnu uses: actions/download-artifact@v3 with: @@ -400,6 +447,9 @@ jobs: path: target_releases/ - name: Integration Tests run: | + if [[ "${{ matrix.sslmode }}" == "verify-ca" || "${{ matrix.sslmode }}" == "verify-full" ]]; then + export PGSSLROOTCERT=target/certs/server.crt + fi export MARTIN_BUILD=- export MARTIN_BIN=target_releases/martin export MBTILES_BUILD=- @@ -417,6 +467,9 @@ jobs: - name: Tests Debian package run: | sudo dpkg -i target_releases/debian-x86_64.deb + if [[ "${{ matrix.sslmode }}" == "verify-ca" || "${{ matrix.sslmode }}" == "verify-full" ]]; then + export PGSSLROOTCERT=target/certs/server.crt + fi export MARTIN_BUILD=- export MARTIN_BIN=/usr/bin/martin export MBTILES_BUILD=- @@ -427,6 +480,7 @@ jobs: env: DATABASE_URL: postgres://${{ env.PGUSER }}:${{ env.PGUSER }}@${{ env.PGHOST }}:${{ job.services.postgres.ports[5432] }}/${{ env.PGDATABASE }}?sslmode=${{ matrix.sslmode }} - name: Unit Tests + if: matrix.sslmode != 'verify-ca' && matrix.sslmode != 'verify-full' run: | echo "Running unit tests, connecting to DATABASE_URL=$DATABASE_URL" echo "Same but as base64 to prevent GitHub obfuscation (this is not a secret):" @@ -447,7 +501,7 @@ jobs: package: name: Package ${{ matrix.target }} runs-on: ${{ matrix.os }} - needs: [ docker, test, test-legacy ] + needs: [ lint-debug-test, docker-build-test, test-multi-os, test-with-svc ] strategy: fail-fast: true matrix: diff --git a/Cargo.lock b/Cargo.lock index 77103cd50..7de3d8755 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -495,9 +495,9 @@ dependencies = [ [[package]] name = "brotli" -version = "3.3.4" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" +checksum = "516074a47ef4bce09577a3b379392300159ce5b1ba2e501ff1c819950066100f" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -506,9 +506,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "2.3.4" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b6561fd3f895a11e8f72af2cb7d22e08366bebc2b6b57f7744c4bda27034744" +checksum = "da74e2b81409b1b743f8f0c62cc6254afefb8b8e50bbfe3735550f7aeefa3448" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -1696,9 +1696,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a9bad9f94746442c783ca431b22403b519cd7fbeed0533fdd6328b2f2212128" +checksum = "3852614a3bd9ca9804678ba6be5e3b8ce76dfc902cae004e3e0c44051b6e88db" [[package]] name = "local-channel" @@ -2394,9 +2394,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.9.5" +version = "1.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" +checksum = "ebee201405406dbf528b8b672104ae6d6d63e6d118cb10e4d51abbc7b58044ff" dependencies = [ "aho-corasick", "memchr", @@ -2406,9 +2406,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" +checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9" dependencies = [ "aho-corasick", "memchr", @@ -2464,9 +2464,9 @@ dependencies = [ [[package]] name = "roxmltree" -version = "0.18.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8f595a457b6b8c6cda66a48503e92ee8d19342f905948f29c383200ec9eb1d8" +checksum = "862340e351ce1b271a378ec53f304a5558f7db87f3769dc655a8f6ecbb68b302" dependencies = [ "xmlparser", ] @@ -3916,9 +3916,9 @@ dependencies = [ [[package]] name = "xmlparser" -version = "0.13.5" +version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d25c75bf9ea12c4040a97f829154768bbbce366287e2dc044af160cd79a13fd" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "xmlwriter" diff --git a/docker-compose.yml b/docker-compose.yml index bda054125..04754d296 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -75,6 +75,34 @@ services: - ./tests/fixtures/initdb-dc-ssl.sh:/docker-entrypoint-initdb.d/10_martin.sh - ./tests/fixtures/initdb-dc.sh:/docker-entrypoint-initdb.d/20_martin.sh + db-ssl-cert: + # This should match the version of postgres used in the CI workflow + image: postgis/postgis:15-3.3 + command: + - "postgres" + - "-c" + - "ssl=on" + - "-c" + - "ssl_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem" + - "-c" + - "ssl_key_file=/etc/ssl/private/ssl-cert-snakeoil.key" + restart: unless-stopped + ports: + - "${PGPORT:-5411}:5432" + environment: + # POSTGRES_* variables are used by the postgis/postgres image + # PG_* variables are used by psql + - POSTGRES_DB=db + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + - PGDATABASE=db + - PGUSER=postgres + - PGPASSWORD=postgres + volumes: + - ./tests/fixtures:/fixtures + - ./tests/fixtures/initdb-dc-ssl-cert.sh:/docker-entrypoint-initdb.d/10_martin.sh + - ./tests/fixtures/initdb-dc.sh:/docker-entrypoint-initdb.d/20_martin.sh + db-legacy: # This should match the version of postgres used in the CI workflow image: postgis/postgis:11-3.0-alpine diff --git a/justfile b/justfile index 08799320e..b5484a2e5 100644 --- a/justfile +++ b/justfile @@ -43,18 +43,25 @@ clean-test: rm -rf tests/output # Start a test database -start: (docker-up "db") +start: (docker-up "db") docker-is-ready # Start an ssl-enabled test database -start-ssl: (docker-up "db-ssl") +start-ssl: (docker-up "db-ssl") docker-is-ready + +# Start an ssl-enabled test database that requires a client certificate +start-ssl-cert: (docker-up "db-ssl-cert") docker-is-ready # Start a legacy test database -start-legacy: (docker-up "db-legacy") +start-legacy: (docker-up "db-legacy") docker-is-ready # Start a specific test database, e.g. db or db-legacy [private] docker-up name: docker-compose up -d {{ name }} + +# Wait for the test database to be ready +[private] +docker-is-ready: docker-compose run -T --rm db-is-ready alias _down := stop @@ -87,6 +94,22 @@ test: start test-unit test-int test-ssl: start-ssl test-unit clean-test tests/test.sh +# Run all tests using an SSL connection with client cert to a test database. Expected output won't match. +test-ssl-cert: start-ssl-cert + #!/usr/bin/env bash + set -euxo pipefail + # copy client cert to the tests folder from the docker container + KEY_DIR=target/certs + mkdir -p $KEY_DIR + docker cp martin-db-ssl-cert-1:/etc/ssl/certs/ssl-cert-snakeoil.pem $KEY_DIR/ssl-cert-snakeoil.pem + docker cp martin-db-ssl-cert-1:/etc/ssl/private/ssl-cert-snakeoil.key $KEY_DIR/ssl-cert-snakeoil.key + # export DATABASE_URL="$DATABASE_URL?sslmode=verify-full&sslrootcert=$KEY_DIR/ssl-cert-snakeoil.pem&sslcert=$KEY_DIR/ssl-cert-snakeoil.pem&sslkey=$KEY_DIR/ssl-cert-snakeoil.key" + export PGSSLROOTCERT="$KEY_DIR/ssl-cert-snakeoil.pem" + export PGSSLCERT="$KEY_DIR/ssl-cert-snakeoil.pem" + export PGSSLKEY="$KEY_DIR/ssl-cert-snakeoil.key" + {{just_executable()}} test-unit clean-test + tests/test.sh + # Run all tests using the oldest supported version of the database test-legacy: start-legacy test-unit test-int diff --git a/tests/fixtures/initdb-dc-ssl-cert.sh b/tests/fixtures/initdb-dc-ssl-cert.sh new file mode 100755 index 000000000..84ba461b9 --- /dev/null +++ b/tests/fixtures/initdb-dc-ssl-cert.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env sh +set -e + +mv /var/lib/postgresql/data/pg_hba.conf /var/lib/postgresql/data/pg_hba.conf.bak +cat > /var/lib/postgresql/data/pg_hba.conf <&1 | tee test_log_1.txt & +$MARTIN_BIN "${ARG[@]}" 2>&1 | tee "${TMP_DIR}/test_log_1.txt" & PROCESS_ID=`jobs -p` { set +x; } 2> /dev/null @@ -227,7 +230,7 @@ test_pbf mb_mvt_2_3_1 world_cities/2/3/1 test_pbf points_empty_srid_0_0_0 points_empty_srid/0/0/0 kill_process $PROCESS_ID -validate_log test_log_1.txt +validate_log "${TMP_DIR}/test_log_1.txt" echo "------------------------------------------------------------------------------------------------------------------------" @@ -237,7 +240,7 @@ mkdir -p "$TEST_OUT_DIR" ARG=(--config tests/config.yaml --max-feature-count 1000 --save-config "$(dirname "$0")/output/given_config.yaml" -W 1) set -x -$MARTIN_BIN "${ARG[@]}" 2>&1 | tee test_log_2.txt & +$MARTIN_BIN "${ARG[@]}" 2>&1 | tee "${TMP_DIR}/test_log_2.txt" & PROCESS_ID=`jobs -p` { set +x; } 2> /dev/null trap "kill -9 $PROCESS_ID 2> /dev/null || true" EXIT @@ -266,7 +269,7 @@ test_jsn spr_cmp_2x sprite/src1,mysrc@2x.json test_png spr_cmp_2x sprite/src1,mysrc@2x.png kill_process $PROCESS_ID -validate_log test_log_2.txt +validate_log "${TMP_DIR}/test_log_2.txt" remove_line "$(dirname "$0")/output/given_config.yaml" " connection_string: " remove_line "$(dirname "$0")/output/generated_config.yaml" " connection_string: " From 3adc896b9625186821a62948db4fe2ae9e97f8c3 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Sat, 30 Sep 2023 23:08:44 -0400 Subject: [PATCH 018/108] version --- Cargo.lock | 2 +- martin/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7de3d8755..46fd8cd27 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1735,7 +1735,7 @@ checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "martin" -version = "0.9.0-pre.3" +version = "0.9.0" dependencies = [ "actix", "actix-cors", diff --git a/martin/Cargo.toml b/martin/Cargo.toml index c3ea8cb73..62119dc29 100644 --- a/martin/Cargo.toml +++ b/martin/Cargo.toml @@ -2,7 +2,7 @@ name = "martin" # Make sure to update /home/nyurik/dev/rust/martin/homebrew-formula/martin.rb version # Once the release is published with the hash -version = "0.9.0-pre.3" +version = "0.9.0" authors = ["Stepan Kuzmin ", "Yuri Astrakhan ", "MapLibre contributors"] description = "Blazing fast and lightweight tile server with PostGIS, MBTiles, and PMTiles support" keywords = ["maps", "tiles", "mbtiles", "pmtiles", "postgis"] From 14ea5cb2f695c0963a6afac82f0c6b0d532f7478 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Sun, 1 Oct 2023 19:55:26 -0400 Subject: [PATCH 019/108] Update README.md --- README.md | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index df2442e10..35527cc63 100755 --- a/README.md +++ b/README.md @@ -23,14 +23,18 @@ _See [installation instructions](https://maplibre.org/martin/installation.html) You can download martin from [GitHub releases page](https://github.com/maplibre/martin/releases). -| Platform | Downloads (latest) | -|----------|------------------------| -| Linux | [64-bit][rl-linux-tar] | -| macOS | [64-bit][rl-macos-tar] | -| Windows | [64-bit][rl-win64-zip] | - -[rl-linux-tar]: https://github.com/maplibre/martin/releases/latest/download/martin-Linux-x86_64.tar.gz -[rl-macos-tar]: https://github.com/maplibre/martin/releases/latest/download/martin-Darwin-x86_64.tar.gz +| Platform | AMD-64 | ARM-64 | +|----------|----------------------------------------------------------------------------------------------|-------------------------------------| +| Linux | [.tar.gz][rl-linux-x64] (gnu), [.tar.gz][rl-linux-x64-musl] (musl), [.deb][rl-linux-x64-deb] | [.tar.gz][rl-linux-a64-musl] (musl) | +| macOS | [.tar.gz][rl-macos-x64] | [.tar.gz][rl-macos-a64] | +| Windows | [.zip][rl-win64-zip] | | + +[rl-linux-x64]: https://github.com/maplibre/martin/releases/latest/download/martin-Linux-x86_64.tar.gz +[rl-linux-x64-musl]: https://github.com/maplibre/martin/releases/latest/download/martin-Linux-x86_64-musl.tar.gz +[rl-linux-x64-deb]: https://github.com/maplibre/martin/releases/latest/download/martin-Debian-x86_64.deb +[rl-linux-a64-musl]: https://github.com/maplibre/martin/releases/latest/download/martin-Linux-aarch64-musl.tar.gz +[rl-macos-x64]: https://github.com/maplibre/martin/releases/latest/download/martin-Darwin-x86_64.tar.gz +[rl-macos-a64]: https://github.com/maplibre/martin/releases/latest/download/martin-Darwin-aarch64.tar.gz [rl-win64-zip]: https://github.com/maplibre/martin/releases/latest/download/martin-Windows-x86_64.zip If you are using macOS and [Homebrew](https://brew.sh/) you can install martin using Homebrew tap. From 6b7bcabe49946401d727540f37d8d613bb439424 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Sun, 1 Oct 2023 23:28:43 -0400 Subject: [PATCH 020/108] Cleanup mbtiles, rename TileCopierOptions, testing (#916) * Rename `TileCopierOptions` -> `TileCopier` * remove a few un-needed sqlite open to detect mbtiles type * move `open_and_detect_type` to `MBTiles` * add `attach_to` to `MBTiles` * move various table creation fn to mbtiles_queries file * a few sql format --- Cargo.lock | 2 +- Cargo.toml | 2 +- ...7989d706ce91630c9332a600e8022c0d4b628.json | 20 -- ...29e7f1af10037bdfb6543b029cc80c3ee60dd.json | 20 -- ...b59995c337cfe7e4c54d4bbb2669a27682401.json | 20 -- ...b7766f876ddb9a357a512ab3a37914bea003c.json | 12 - ...3c46f61ff92ffbc6ec3bba4860abd60d224cb.json | 12 - ...37901cbe4b6421bac3cf671e86d4b5d8dc0f3.json | 20 ++ ...8098085994c8fba93e0293359afd43079c50c.json | 20 ++ ...ba30fbf018805fe9ca2acd2b2e225183d1f13.json | 20 ++ ...7e779ecf324e1862945fbd18da4bf5baf565b.json | 12 - ...ad480aaa224721cd9ddb4ac5859f71a57727e.json | 12 - ...2ee47cfc72b56f6ed275a0b0688047405498f.json | 12 - ...1f52ce710d8978e3b35b59b724fc5bee9f55c.json | 12 - martin-mbtiles/Cargo.toml | 2 +- martin-mbtiles/src/bin/main.rs | 22 +- martin-mbtiles/src/lib.rs | 2 +- martin-mbtiles/src/mbtiles.rs | 143 +++++---- martin-mbtiles/src/mbtiles_queries.rs | 249 +++++++++++---- martin-mbtiles/src/tile_copier.rs | 292 ++++++------------ 20 files changed, 434 insertions(+), 472 deletions(-) delete mode 100644 martin-mbtiles/.sqlx/query-14f262aafedb8739ee403fe6fc67989d706ce91630c9332a600e8022c0d4b628.json delete mode 100644 martin-mbtiles/.sqlx/query-177aed5e4ee0e7a23eb708174a829e7f1af10037bdfb6543b029cc80c3ee60dd.json delete mode 100644 martin-mbtiles/.sqlx/query-3a1e6e16157856190e061e1ade9b59995c337cfe7e4c54d4bbb2669a27682401.json delete mode 100644 martin-mbtiles/.sqlx/query-3b2930e8d61f31ea1bf32efe340b7766f876ddb9a357a512ab3a37914bea003c.json delete mode 100644 martin-mbtiles/.sqlx/query-45de99a3628a53940ef80b0e2603c46f61ff92ffbc6ec3bba4860abd60d224cb.json create mode 100644 martin-mbtiles/.sqlx/query-4905d37cd3818e2fe9f65fdd20437901cbe4b6421bac3cf671e86d4b5d8dc0f3.json create mode 100644 martin-mbtiles/.sqlx/query-7341bfc10beb4719811556a57ae8098085994c8fba93e0293359afd43079c50c.json create mode 100644 martin-mbtiles/.sqlx/query-809e89c3b223e28c6716d405e13ba30fbf018805fe9ca2acd2b2e225183d1f13.json delete mode 100644 martin-mbtiles/.sqlx/query-a115609880b2c6ed3beeb5aaf8c7e779ecf324e1862945fbd18da4bf5baf565b.json delete mode 100644 martin-mbtiles/.sqlx/query-b3aaef71d6a26404c3bebcc6ee8ad480aaa224721cd9ddb4ac5859f71a57727e.json delete mode 100644 martin-mbtiles/.sqlx/query-d1d61dfa7c34dafb4588f78e23b2ee47cfc72b56f6ed275a0b0688047405498f.json delete mode 100644 martin-mbtiles/.sqlx/query-e13e2e17d5bf56287bc0fd7c55a1f52ce710d8978e3b35b59b724fc5bee9f55c.json diff --git a/Cargo.lock b/Cargo.lock index 46fd8cd27..207615c35 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1781,7 +1781,7 @@ dependencies = [ [[package]] name = "martin-mbtiles" -version = "0.5.0" +version = "0.6.0" dependencies = [ "actix-rt", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index 9893e655d..173ab1b41 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ indoc = "2" itertools = "0.11" json-patch = "1.1" log = "0.4" -martin-mbtiles = { path = "./martin-mbtiles", version = "0.5.0", default-features = false } +martin-mbtiles = { path = "./martin-mbtiles", version = "0.6.0", default-features = false } martin-tile-utils = { path = "./martin-tile-utils", version = "0.1.0" } num_cpus = "1" pmtiles = { version = "0.3", features = ["mmap-async-tokio", "tilejson"] } diff --git a/martin-mbtiles/.sqlx/query-14f262aafedb8739ee403fe6fc67989d706ce91630c9332a600e8022c0d4b628.json b/martin-mbtiles/.sqlx/query-14f262aafedb8739ee403fe6fc67989d706ce91630c9332a600e8022c0d4b628.json deleted file mode 100644 index 0848bf78d..000000000 --- a/martin-mbtiles/.sqlx/query-14f262aafedb8739ee403fe6fc67989d706ce91630c9332a600e8022c0d4b628.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT (\n -- Has a \"map\" table\n SELECT COUNT(*) = 1\n FROM sqlite_master\n WHERE name = 'map'\n AND type = 'table'\n --\n ) AND (\n -- \"map\" table's columns and their types are as expected:\n -- 4 columns (zoom_level, tile_column, tile_row, tile_id).\n -- The order is not important\n SELECT COUNT(*) = 4\n FROM pragma_table_info('map')\n WHERE ((name = \"zoom_level\" AND type = \"INTEGER\")\n OR (name = \"tile_column\" AND type = \"INTEGER\")\n OR (name = \"tile_row\" AND type = \"INTEGER\")\n OR (name = \"tile_id\" AND type = \"TEXT\"))\n --\n ) AND (\n -- Has a \"images\" table\n SELECT COUNT(*) = 1\n FROM sqlite_master\n WHERE name = 'images'\n AND type = 'table'\n --\n ) AND (\n -- \"images\" table's columns and their types are as expected:\n -- 2 columns (tile_id, tile_data).\n -- The order is not important\n SELECT COUNT(*) = 2\n FROM pragma_table_info('images')\n WHERE ((name = \"tile_id\" AND type = \"TEXT\")\n OR (name = \"tile_data\" AND type = \"BLOB\"))\n --\n ) AS is_valid;\n", - "describe": { - "columns": [ - { - "name": "is_valid", - "ordinal": 0, - "type_info": "Int" - } - ], - "parameters": { - "Right": 0 - }, - "nullable": [ - null - ] - }, - "hash": "14f262aafedb8739ee403fe6fc67989d706ce91630c9332a600e8022c0d4b628" -} diff --git a/martin-mbtiles/.sqlx/query-177aed5e4ee0e7a23eb708174a829e7f1af10037bdfb6543b029cc80c3ee60dd.json b/martin-mbtiles/.sqlx/query-177aed5e4ee0e7a23eb708174a829e7f1af10037bdfb6543b029cc80c3ee60dd.json deleted file mode 100644 index 6e141d9d8..000000000 --- a/martin-mbtiles/.sqlx/query-177aed5e4ee0e7a23eb708174a829e7f1af10037bdfb6543b029cc80c3ee60dd.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT (\n -- Has a \"tiles\" table\n SELECT COUNT(*) = 1\n FROM sqlite_master\n WHERE name = 'tiles'\n AND type = 'table'\n --\n ) AND (\n -- \"tiles\" table's columns and their types are as expected:\n -- 4 columns (zoom_level, tile_column, tile_row, tile_data).\n -- The order is not important\n SELECT COUNT(*) = 4\n FROM pragma_table_info('tiles')\n WHERE ((name = \"zoom_level\" AND type = \"INTEGER\")\n OR (name = \"tile_column\" AND type = \"INTEGER\")\n OR (name = \"tile_row\" AND type = \"INTEGER\")\n OR (name = \"tile_data\" AND type = \"BLOB\"))\n --\n ) as is_valid;\n", - "describe": { - "columns": [ - { - "name": "is_valid", - "ordinal": 0, - "type_info": "Int" - } - ], - "parameters": { - "Right": 0 - }, - "nullable": [ - null - ] - }, - "hash": "177aed5e4ee0e7a23eb708174a829e7f1af10037bdfb6543b029cc80c3ee60dd" -} diff --git a/martin-mbtiles/.sqlx/query-3a1e6e16157856190e061e1ade9b59995c337cfe7e4c54d4bbb2669a27682401.json b/martin-mbtiles/.sqlx/query-3a1e6e16157856190e061e1ade9b59995c337cfe7e4c54d4bbb2669a27682401.json deleted file mode 100644 index 6230f16d4..000000000 --- a/martin-mbtiles/.sqlx/query-3a1e6e16157856190e061e1ade9b59995c337cfe7e4c54d4bbb2669a27682401.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT (\n -- Has a \"tiles_with_hash\" table\n SELECT COUNT(*) = 1\n FROM sqlite_master\n WHERE name = 'tiles_with_hash'\n AND type = 'table'\n --\n ) AND (\n -- \"tiles_with_hash\" table's columns and their types are as expected:\n -- 5 columns (zoom_level, tile_column, tile_row, tile_data, tile_hash).\n -- The order is not important\n SELECT COUNT(*) = 5\n FROM pragma_table_info('tiles_with_hash')\n WHERE ((name = \"zoom_level\" AND type = \"INTEGER\")\n OR (name = \"tile_column\" AND type = \"INTEGER\")\n OR (name = \"tile_row\" AND type = \"INTEGER\")\n OR (name = \"tile_data\" AND type = \"BLOB\")\n OR (name = \"tile_hash\" AND type = \"TEXT\"))\n --\n ) as is_valid;\n", - "describe": { - "columns": [ - { - "name": "is_valid", - "ordinal": 0, - "type_info": "Int" - } - ], - "parameters": { - "Right": 0 - }, - "nullable": [ - null - ] - }, - "hash": "3a1e6e16157856190e061e1ade9b59995c337cfe7e4c54d4bbb2669a27682401" -} diff --git a/martin-mbtiles/.sqlx/query-3b2930e8d61f31ea1bf32efe340b7766f876ddb9a357a512ab3a37914bea003c.json b/martin-mbtiles/.sqlx/query-3b2930e8d61f31ea1bf32efe340b7766f876ddb9a357a512ab3a37914bea003c.json deleted file mode 100644 index 1a8fe4fd0..000000000 --- a/martin-mbtiles/.sqlx/query-3b2930e8d61f31ea1bf32efe340b7766f876ddb9a357a512ab3a37914bea003c.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "ATTACH DATABASE ? AS sourceDb", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "3b2930e8d61f31ea1bf32efe340b7766f876ddb9a357a512ab3a37914bea003c" -} diff --git a/martin-mbtiles/.sqlx/query-45de99a3628a53940ef80b0e2603c46f61ff92ffbc6ec3bba4860abd60d224cb.json b/martin-mbtiles/.sqlx/query-45de99a3628a53940ef80b0e2603c46f61ff92ffbc6ec3bba4860abd60d224cb.json deleted file mode 100644 index 7f4b7b65a..000000000 --- a/martin-mbtiles/.sqlx/query-45de99a3628a53940ef80b0e2603c46f61ff92ffbc6ec3bba4860abd60d224cb.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "ATTACH DATABASE ? AS srcDb", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "45de99a3628a53940ef80b0e2603c46f61ff92ffbc6ec3bba4860abd60d224cb" -} diff --git a/martin-mbtiles/.sqlx/query-4905d37cd3818e2fe9f65fdd20437901cbe4b6421bac3cf671e86d4b5d8dc0f3.json b/martin-mbtiles/.sqlx/query-4905d37cd3818e2fe9f65fdd20437901cbe4b6421bac3cf671e86d4b5d8dc0f3.json new file mode 100644 index 000000000..47392647f --- /dev/null +++ b/martin-mbtiles/.sqlx/query-4905d37cd3818e2fe9f65fdd20437901cbe4b6421bac3cf671e86d4b5d8dc0f3.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "SELECT (\n -- Has a 'tiles_with_hash' table\n SELECT COUNT(*) = 1\n FROM sqlite_master\n WHERE name = 'tiles_with_hash'\n AND type = 'table'\n --\n ) AND (\n -- 'tiles_with_hash' table's columns and their types are as expected:\n -- 5 columns (zoom_level, tile_column, tile_row, tile_data, tile_hash).\n -- The order is not important\n SELECT COUNT(*) = 5\n FROM pragma_table_info('tiles_with_hash')\n WHERE ((name = 'zoom_level' AND type = 'INTEGER')\n OR (name = 'tile_column' AND type = 'INTEGER')\n OR (name = 'tile_row' AND type = 'INTEGER')\n OR (name = 'tile_data' AND type = 'BLOB')\n OR (name = 'tile_hash' AND type = 'TEXT'))\n --\n ) as is_valid;", + "describe": { + "columns": [ + { + "name": "is_valid", + "ordinal": 0, + "type_info": "Int" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + null + ] + }, + "hash": "4905d37cd3818e2fe9f65fdd20437901cbe4b6421bac3cf671e86d4b5d8dc0f3" +} diff --git a/martin-mbtiles/.sqlx/query-7341bfc10beb4719811556a57ae8098085994c8fba93e0293359afd43079c50c.json b/martin-mbtiles/.sqlx/query-7341bfc10beb4719811556a57ae8098085994c8fba93e0293359afd43079c50c.json new file mode 100644 index 000000000..2b9d7474d --- /dev/null +++ b/martin-mbtiles/.sqlx/query-7341bfc10beb4719811556a57ae8098085994c8fba93e0293359afd43079c50c.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "SELECT (\n -- Has a 'tiles' table\n SELECT COUNT(*) = 1\n FROM sqlite_master\n WHERE name = 'tiles'\n AND type = 'table'\n --\n ) AND (\n -- 'tiles' table's columns and their types are as expected:\n -- 4 columns (zoom_level, tile_column, tile_row, tile_data).\n -- The order is not important\n SELECT COUNT(*) = 4\n FROM pragma_table_info('tiles')\n WHERE ((name = 'zoom_level' AND type = 'INTEGER')\n OR (name = 'tile_column' AND type = 'INTEGER')\n OR (name = 'tile_row' AND type = 'INTEGER')\n OR (name = 'tile_data' AND type = 'BLOB'))\n --\n ) as is_valid;", + "describe": { + "columns": [ + { + "name": "is_valid", + "ordinal": 0, + "type_info": "Int" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + null + ] + }, + "hash": "7341bfc10beb4719811556a57ae8098085994c8fba93e0293359afd43079c50c" +} diff --git a/martin-mbtiles/.sqlx/query-809e89c3b223e28c6716d405e13ba30fbf018805fe9ca2acd2b2e225183d1f13.json b/martin-mbtiles/.sqlx/query-809e89c3b223e28c6716d405e13ba30fbf018805fe9ca2acd2b2e225183d1f13.json new file mode 100644 index 000000000..faf6b51f7 --- /dev/null +++ b/martin-mbtiles/.sqlx/query-809e89c3b223e28c6716d405e13ba30fbf018805fe9ca2acd2b2e225183d1f13.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "SELECT (\n -- Has a 'map' table\n SELECT COUNT(*) = 1\n FROM sqlite_master\n WHERE name = 'map'\n AND type = 'table'\n --\n ) AND (\n -- 'map' table's columns and their types are as expected:\n -- 4 columns (zoom_level, tile_column, tile_row, tile_id).\n -- The order is not important\n SELECT COUNT(*) = 4\n FROM pragma_table_info('map')\n WHERE ((name = 'zoom_level' AND type = 'INTEGER')\n OR (name = 'tile_column' AND type = 'INTEGER')\n OR (name = 'tile_row' AND type = 'INTEGER')\n OR (name = 'tile_id' AND type = 'TEXT'))\n --\n ) AND (\n -- Has a 'images' table\n SELECT COUNT(*) = 1\n FROM sqlite_master\n WHERE name = 'images'\n AND type = 'table'\n --\n ) AND (\n -- 'images' table's columns and their types are as expected:\n -- 2 columns (tile_id, tile_data).\n -- The order is not important\n SELECT COUNT(*) = 2\n FROM pragma_table_info('images')\n WHERE ((name = 'tile_id' AND type = 'TEXT')\n OR (name = 'tile_data' AND type = 'BLOB'))\n --\n ) AS is_valid;", + "describe": { + "columns": [ + { + "name": "is_valid", + "ordinal": 0, + "type_info": "Int" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + null + ] + }, + "hash": "809e89c3b223e28c6716d405e13ba30fbf018805fe9ca2acd2b2e225183d1f13" +} diff --git a/martin-mbtiles/.sqlx/query-a115609880b2c6ed3beeb5aaf8c7e779ecf324e1862945fbd18da4bf5baf565b.json b/martin-mbtiles/.sqlx/query-a115609880b2c6ed3beeb5aaf8c7e779ecf324e1862945fbd18da4bf5baf565b.json deleted file mode 100644 index fc0b3c0c2..000000000 --- a/martin-mbtiles/.sqlx/query-a115609880b2c6ed3beeb5aaf8c7e779ecf324e1862945fbd18da4bf5baf565b.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "ATTACH DATABASE ? AS newDb", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "a115609880b2c6ed3beeb5aaf8c7e779ecf324e1862945fbd18da4bf5baf565b" -} diff --git a/martin-mbtiles/.sqlx/query-b3aaef71d6a26404c3bebcc6ee8ad480aaa224721cd9ddb4ac5859f71a57727e.json b/martin-mbtiles/.sqlx/query-b3aaef71d6a26404c3bebcc6ee8ad480aaa224721cd9ddb4ac5859f71a57727e.json deleted file mode 100644 index 71fbbc367..000000000 --- a/martin-mbtiles/.sqlx/query-b3aaef71d6a26404c3bebcc6ee8ad480aaa224721cd9ddb4ac5859f71a57727e.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "ATTACH DATABASE ? AS otherDb", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "b3aaef71d6a26404c3bebcc6ee8ad480aaa224721cd9ddb4ac5859f71a57727e" -} diff --git a/martin-mbtiles/.sqlx/query-d1d61dfa7c34dafb4588f78e23b2ee47cfc72b56f6ed275a0b0688047405498f.json b/martin-mbtiles/.sqlx/query-d1d61dfa7c34dafb4588f78e23b2ee47cfc72b56f6ed275a0b0688047405498f.json deleted file mode 100644 index 1d9e5c432..000000000 --- a/martin-mbtiles/.sqlx/query-d1d61dfa7c34dafb4588f78e23b2ee47cfc72b56f6ed275a0b0688047405498f.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "ATTACH DATABASE ? AS originalDb", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "d1d61dfa7c34dafb4588f78e23b2ee47cfc72b56f6ed275a0b0688047405498f" -} diff --git a/martin-mbtiles/.sqlx/query-e13e2e17d5bf56287bc0fd7c55a1f52ce710d8978e3b35b59b724fc5bee9f55c.json b/martin-mbtiles/.sqlx/query-e13e2e17d5bf56287bc0fd7c55a1f52ce710d8978e3b35b59b724fc5bee9f55c.json deleted file mode 100644 index 5d8f76197..000000000 --- a/martin-mbtiles/.sqlx/query-e13e2e17d5bf56287bc0fd7c55a1f52ce710d8978e3b35b59b724fc5bee9f55c.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "ATTACH DATABASE ? AS diffDb", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "e13e2e17d5bf56287bc0fd7c55a1f52ce710d8978e3b35b59b724fc5bee9f55c" -} diff --git a/martin-mbtiles/Cargo.toml b/martin-mbtiles/Cargo.toml index ef6a59111..155792850 100644 --- a/martin-mbtiles/Cargo.toml +++ b/martin-mbtiles/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "martin-mbtiles" -version = "0.5.0" +version = "0.6.0" authors = ["Yuri Astrakhan ", "MapLibre contributors"] description = "A simple low-level MbTiles access and processing library, with some tile format detection and other relevant heuristics." keywords = ["mbtiles", "maps", "tiles", "mvt", "tilejson"] diff --git a/martin-mbtiles/src/bin/main.rs b/martin-mbtiles/src/bin/main.rs index 4df966ce1..96f7d53b9 100644 --- a/martin-mbtiles/src/bin/main.rs +++ b/martin-mbtiles/src/bin/main.rs @@ -2,9 +2,7 @@ use std::path::{Path, PathBuf}; use clap::{Parser, Subcommand}; use log::{error, LevelFilter}; -use martin_mbtiles::{ - apply_mbtiles_diff, IntegrityCheckType, MbtResult, Mbtiles, TileCopierOptions, -}; +use martin_mbtiles::{apply_mbtiles_diff, IntegrityCheckType, MbtResult, Mbtiles, TileCopier}; #[derive(Parser, PartialEq, Eq, Debug)] #[command( @@ -48,7 +46,7 @@ enum Commands { }, /// Copy tiles from one mbtiles file to another. #[command(name = "copy")] - Copy(TileCopierOptions), + Copy(TileCopier), /// Apply diff file generated from 'copy' command #[command(name = "apply-diff")] ApplyDiff { @@ -165,7 +163,7 @@ mod tests { use clap::error::ErrorKind; use clap::Parser; - use martin_mbtiles::{CopyDuplicateMode, TileCopierOptions}; + use martin_mbtiles::{CopyDuplicateMode, TileCopier}; use crate::Commands::{ApplyDiff, Copy, MetaGetValue, MetaSetValue, Validate}; use crate::{Args, IntegrityCheckType}; @@ -186,7 +184,7 @@ mod tests { Args::parse_from(["mbtiles", "copy", "src_file", "dst_file"]), Args { verbose: false, - command: Copy(TileCopierOptions::new( + command: Copy(TileCopier::new( PathBuf::from("src_file"), PathBuf::from("dst_file") )) @@ -210,7 +208,7 @@ mod tests { Args { verbose: false, command: Copy( - TileCopierOptions::new(PathBuf::from("src_file"), PathBuf::from("dst_file")) + TileCopier::new(PathBuf::from("src_file"), PathBuf::from("dst_file")) .min_zoom(Some(1)) .max_zoom(Some(100)) ) @@ -270,7 +268,7 @@ mod tests { Args { verbose: false, command: Copy( - TileCopierOptions::new(PathBuf::from("src_file"), PathBuf::from("dst_file")) + TileCopier::new(PathBuf::from("src_file"), PathBuf::from("dst_file")) .zoom_levels(vec![1, 3, 7]) ) } @@ -291,7 +289,7 @@ mod tests { Args { verbose: false, command: Copy( - TileCopierOptions::new(PathBuf::from("src_file"), PathBuf::from("dst_file")) + TileCopier::new(PathBuf::from("src_file"), PathBuf::from("dst_file")) .diff_with_file(PathBuf::from("no_file")) ) } @@ -312,7 +310,7 @@ mod tests { Args { verbose: false, command: Copy( - TileCopierOptions::new(PathBuf::from("src_file"), PathBuf::from("dst_file")) + TileCopier::new(PathBuf::from("src_file"), PathBuf::from("dst_file")) .on_duplicate(CopyDuplicateMode::Override) ) } @@ -333,7 +331,7 @@ mod tests { Args { verbose: false, command: Copy( - TileCopierOptions::new(PathBuf::from("src_file"), PathBuf::from("dst_file")) + TileCopier::new(PathBuf::from("src_file"), PathBuf::from("dst_file")) .on_duplicate(CopyDuplicateMode::Ignore) ) } @@ -354,7 +352,7 @@ mod tests { Args { verbose: false, command: Copy( - TileCopierOptions::new(PathBuf::from("src_file"), PathBuf::from("dst_file")) + TileCopier::new(PathBuf::from("src_file"), PathBuf::from("dst_file")) .on_duplicate(CopyDuplicateMode::Abort) ) } diff --git a/martin-mbtiles/src/lib.rs b/martin-mbtiles/src/lib.rs index 65522b564..c88a8439e 100644 --- a/martin-mbtiles/src/lib.rs +++ b/martin-mbtiles/src/lib.rs @@ -10,6 +10,6 @@ mod mbtiles_pool; pub use mbtiles_pool::MbtilesPool; mod tile_copier; -pub use tile_copier::{apply_mbtiles_diff, CopyDuplicateMode, TileCopierOptions}; +pub use tile_copier::{apply_mbtiles_diff, CopyDuplicateMode, TileCopier}; mod mbtiles_queries; diff --git a/martin-mbtiles/src/mbtiles.rs b/martin-mbtiles/src/mbtiles.rs index a3d350dc9..c6f69892b 100644 --- a/martin-mbtiles/src/mbtiles.rs +++ b/martin-mbtiles/src/mbtiles.rs @@ -123,6 +123,19 @@ impl Mbtiles { } } + /// Attach this MBTiles file to the given SQLite connection as a given name + pub async fn attach_to(&self, conn: &mut T, name: &str) -> MbtResult<()> + where + for<'e> &'e mut T: SqliteExecutor<'e>, + { + query(&format!("ATTACH DATABASE ? AS {name}")) + .bind(self.filepath()) + .execute(conn) + .await?; + Ok(()) + } + + /// Get a single metadata value from the metadata table pub async fn get_metadata_value(&self, conn: &mut T, key: &str) -> MbtResult> where for<'e> &'e mut T: SqliteExecutor<'e>, @@ -359,6 +372,11 @@ impl Mbtiles { Ok(None) } + pub async fn open_and_detect_type(&self) -> MbtResult { + let mut conn = self.open_with_hashes(true).await?; + self.detect_type(&mut conn).await + } + pub async fn detect_type(&self, conn: &mut T) -> MbtResult where for<'e> &'e mut T: SqliteExecutor<'e>, @@ -583,7 +601,8 @@ where } pub async fn attach_hash_fn(conn: &mut SqliteConnection) -> MbtResult<()> { - let handle = conn.lock_handle().await?.as_raw_handle().as_ptr(); + let mut handle_lock = conn.lock_handle().await?; + let handle = handle_lock.as_raw_handle().as_ptr(); // Safety: we know that the handle is a SQLite connection is locked and is not used anywhere else. // The registered functions will be dropped when SQLX drops DB connection. let rc = unsafe { sqlite_hashes::rusqlite::Connection::from_handle(handle) }?; @@ -600,25 +619,26 @@ mod tests { use super::*; - async fn open(filepath: &str) -> (SqliteConnection, Mbtiles) { - let mbt = Mbtiles::new(filepath).unwrap(); - let mut conn = SqliteConnection::connect(mbt.filepath()).await.unwrap(); - attach_hash_fn(&mut conn).await.unwrap(); - (conn, mbt) + async fn open(filepath: &str) -> MbtResult<(SqliteConnection, Mbtiles)> { + let mbt = Mbtiles::new(filepath)?; + let mut conn = SqliteConnection::connect(mbt.filepath()).await?; + attach_hash_fn(&mut conn).await?; + Ok((conn, mbt)) } #[actix_rt::test] - async fn mbtiles_meta() { + async fn mbtiles_meta() -> MbtResult<()> { let filepath = "../tests/fixtures/mbtiles/geography-class-jpg.mbtiles"; - let mbt = Mbtiles::new(filepath).unwrap(); + let mbt = Mbtiles::new(filepath)?; assert_eq!(mbt.filepath(), filepath); assert_eq!(mbt.filename(), "geography-class-jpg"); + Ok(()) } #[actix_rt::test] - async fn metadata_jpeg() { - let (mut conn, mbt) = open("../tests/fixtures/mbtiles/geography-class-jpg.mbtiles").await; - let metadata = mbt.get_metadata(&mut conn).await.unwrap(); + async fn metadata_jpeg() -> MbtResult<()> { + let (mut conn, mbt) = open("../tests/fixtures/mbtiles/geography-class-jpg.mbtiles").await?; + let metadata = mbt.get_metadata(&mut conn).await?; let tj = metadata.tilejson; assert_eq!(tj.description.unwrap(), "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. "); @@ -630,12 +650,13 @@ mod tests { assert_eq!(tj.version.unwrap(), "1.0.0"); assert_eq!(metadata.id, "geography-class-jpg"); assert_eq!(metadata.tile_info, Format::Jpeg.into()); + Ok(()) } #[actix_rt::test] - async fn metadata_mvt() { - let (mut conn, mbt) = open("../tests/fixtures/mbtiles/world_cities.mbtiles").await; - let metadata = mbt.get_metadata(&mut conn).await.unwrap(); + async fn metadata_mvt() -> MbtResult<()> { + let (mut conn, mbt) = open("../tests/fixtures/mbtiles/world_cities.mbtiles").await?; + let metadata = mbt.get_metadata(&mut conn).await?; let tj = metadata.tilejson; assert_eq!(tj.maxzoom.unwrap(), 6); @@ -661,41 +682,38 @@ mod tests { TileInfo::new(Format::Mvt, Encoding::Gzip) ); assert_eq!(metadata.layer_type, Some("overlay".to_string())); + Ok(()) } #[actix_rt::test] - async fn metadata_get_key() { - let (mut conn, mbt) = open("../tests/fixtures/mbtiles/world_cities.mbtiles").await; - - let res = mbt.get_metadata_value(&mut conn, "bounds").await.unwrap(); - assert_eq!(res.unwrap(), "-123.123590,-37.818085,174.763027,59.352706"); - let res = mbt.get_metadata_value(&mut conn, "name").await.unwrap(); - assert_eq!(res.unwrap(), "Major cities from Natural Earth data"); - let res = mbt.get_metadata_value(&mut conn, "maxzoom").await.unwrap(); - assert_eq!(res.unwrap(), "6"); - let res = mbt.get_metadata_value(&mut conn, "nonexistent_key").await; - assert_eq!(res.unwrap(), None); - let res = mbt.get_metadata_value(&mut conn, "").await; - assert_eq!(res.unwrap(), None); + async fn metadata_get_key() -> MbtResult<()> { + let (mut conn, mbt) = open("../tests/fixtures/mbtiles/world_cities.mbtiles").await?; + + let res = mbt.get_metadata_value(&mut conn, "bounds").await?.unwrap(); + assert_eq!(res, "-123.123590,-37.818085,174.763027,59.352706"); + let res = mbt.get_metadata_value(&mut conn, "name").await?.unwrap(); + assert_eq!(res, "Major cities from Natural Earth data"); + let res = mbt.get_metadata_value(&mut conn, "maxzoom").await?.unwrap(); + assert_eq!(res, "6"); + let res = mbt.get_metadata_value(&mut conn, "nonexistent_key").await?; + assert_eq!(res, None); + let res = mbt.get_metadata_value(&mut conn, "").await?; + assert_eq!(res, None); + Ok(()) } #[actix_rt::test] - async fn metadata_set_key() { - let (mut conn, mbt) = open("file:metadata_set_key_mem_db?mode=memory&cache=shared").await; + async fn metadata_set_key() -> MbtResult<()> { + let (mut conn, mbt) = open("file:metadata_set_key_mem_db?mode=memory&cache=shared").await?; query("CREATE TABLE metadata (name text NOT NULL PRIMARY KEY, value text);") .execute(&mut conn) - .await - .unwrap(); + .await?; mbt.set_metadata_value(&mut conn, "bounds", Some("0.0, 0.0, 0.0, 0.0".to_string())) - .await - .unwrap(); + .await?; assert_eq!( - mbt.get_metadata_value(&mut conn, "bounds") - .await - .unwrap() - .unwrap(), + mbt.get_metadata_value(&mut conn, "bounds").await?.unwrap(), "0.0, 0.0, 0.0, 0.0" ); @@ -704,58 +722,53 @@ mod tests { "bounds", Some("-123.123590,-37.818085,174.763027,59.352706".to_string()), ) - .await - .unwrap(); + .await?; assert_eq!( - mbt.get_metadata_value(&mut conn, "bounds") - .await - .unwrap() - .unwrap(), + mbt.get_metadata_value(&mut conn, "bounds").await?.unwrap(), "-123.123590,-37.818085,174.763027,59.352706" ); - mbt.set_metadata_value(&mut conn, "bounds", None) - .await - .unwrap(); - assert_eq!( - mbt.get_metadata_value(&mut conn, "bounds").await.unwrap(), - None - ); + mbt.set_metadata_value(&mut conn, "bounds", None).await?; + assert_eq!(mbt.get_metadata_value(&mut conn, "bounds").await?, None); + + Ok(()) } #[actix_rt::test] - async fn detect_type() { - let (mut conn, mbt) = open("../tests/fixtures/mbtiles/world_cities.mbtiles").await; - let res = mbt.detect_type(&mut conn).await.unwrap(); + async fn detect_type() -> MbtResult<()> { + let (mut conn, mbt) = open("../tests/fixtures/mbtiles/world_cities.mbtiles").await?; + let res = mbt.detect_type(&mut conn).await?; assert_eq!(res, MbtType::Flat); - let (mut conn, mbt) = open("../tests/fixtures/mbtiles/zoomed_world_cities.mbtiles").await; - let res = mbt.detect_type(&mut conn).await.unwrap(); + let (mut conn, mbt) = open("../tests/fixtures/mbtiles/zoomed_world_cities.mbtiles").await?; + let res = mbt.detect_type(&mut conn).await?; assert_eq!(res, MbtType::FlatWithHash); - let (mut conn, mbt) = open("../tests/fixtures/mbtiles/geography-class-jpg.mbtiles").await; - let res = mbt.detect_type(&mut conn).await.unwrap(); + let (mut conn, mbt) = open("../tests/fixtures/mbtiles/geography-class-jpg.mbtiles").await?; + let res = mbt.detect_type(&mut conn).await?; assert_eq!(res, MbtType::Normalized); - let (mut conn, mbt) = open(":memory:").await; + let (mut conn, mbt) = open(":memory:").await?; let res = mbt.detect_type(&mut conn).await; assert!(matches!(res, Err(MbtError::InvalidDataFormat(_)))); + + Ok(()) } #[actix_rt::test] - async fn validate_valid_file() { - let (mut conn, mbt) = open("../tests/fixtures/mbtiles/zoomed_world_cities.mbtiles").await; - + async fn validate_valid_file() -> MbtResult<()> { + let (mut conn, mbt) = open("../tests/fixtures/mbtiles/zoomed_world_cities.mbtiles").await?; mbt.check_integrity(&mut conn, IntegrityCheckType::Quick) - .await - .unwrap(); + .await?; + Ok(()) } #[actix_rt::test] - async fn validate_invalid_file() { + async fn validate_invalid_file() -> MbtResult<()> { let (mut conn, mbt) = - open("../tests/fixtures/files/invalid_zoomed_world_cities.mbtiles").await; + open("../tests/fixtures/files/invalid_zoomed_world_cities.mbtiles").await?; let result = mbt.check_agg_tiles_hashes(&mut conn).await; assert!(matches!(result, Err(MbtError::AggHashMismatch(..)))); + Ok(()) } } diff --git a/martin-mbtiles/src/mbtiles_queries.rs b/martin-mbtiles/src/mbtiles_queries.rs index 0d04815da..9df2731bf 100644 --- a/martin-mbtiles/src/mbtiles_queries.rs +++ b/martin-mbtiles/src/mbtiles_queries.rs @@ -7,42 +7,41 @@ where for<'e> &'e mut T: SqliteExecutor<'e>, { let sql = query!( - r#"SELECT ( - -- Has a "map" table - SELECT COUNT(*) = 1 - FROM sqlite_master - WHERE name = 'map' - AND type = 'table' - -- - ) AND ( - -- "map" table's columns and their types are as expected: - -- 4 columns (zoom_level, tile_column, tile_row, tile_id). - -- The order is not important - SELECT COUNT(*) = 4 - FROM pragma_table_info('map') - WHERE ((name = "zoom_level" AND type = "INTEGER") - OR (name = "tile_column" AND type = "INTEGER") - OR (name = "tile_row" AND type = "INTEGER") - OR (name = "tile_id" AND type = "TEXT")) - -- - ) AND ( - -- Has a "images" table - SELECT COUNT(*) = 1 - FROM sqlite_master - WHERE name = 'images' - AND type = 'table' - -- - ) AND ( - -- "images" table's columns and their types are as expected: - -- 2 columns (tile_id, tile_data). - -- The order is not important - SELECT COUNT(*) = 2 - FROM pragma_table_info('images') - WHERE ((name = "tile_id" AND type = "TEXT") - OR (name = "tile_data" AND type = "BLOB")) - -- - ) AS is_valid; -"# + "SELECT ( + -- Has a 'map' table + SELECT COUNT(*) = 1 + FROM sqlite_master + WHERE name = 'map' + AND type = 'table' + -- + ) AND ( + -- 'map' table's columns and their types are as expected: + -- 4 columns (zoom_level, tile_column, tile_row, tile_id). + -- The order is not important + SELECT COUNT(*) = 4 + FROM pragma_table_info('map') + WHERE ((name = 'zoom_level' AND type = 'INTEGER') + OR (name = 'tile_column' AND type = 'INTEGER') + OR (name = 'tile_row' AND type = 'INTEGER') + OR (name = 'tile_id' AND type = 'TEXT')) + -- + ) AND ( + -- Has a 'images' table + SELECT COUNT(*) = 1 + FROM sqlite_master + WHERE name = 'images' + AND type = 'table' + -- + ) AND ( + -- 'images' table's columns and their types are as expected: + -- 2 columns (tile_id, tile_data). + -- The order is not important + SELECT COUNT(*) = 2 + FROM pragma_table_info('images') + WHERE ((name = 'tile_id' AND type = 'TEXT') + OR (name = 'tile_data' AND type = 'BLOB')) + -- + ) AS is_valid;" ); Ok(sql @@ -58,27 +57,26 @@ where for<'e> &'e mut T: SqliteExecutor<'e>, { let sql = query!( - r#"SELECT ( - -- Has a "tiles_with_hash" table + "SELECT ( + -- Has a 'tiles_with_hash' table SELECT COUNT(*) = 1 FROM sqlite_master WHERE name = 'tiles_with_hash' - AND type = 'table' + AND type = 'table' -- ) AND ( - -- "tiles_with_hash" table's columns and their types are as expected: + -- 'tiles_with_hash' table's columns and their types are as expected: -- 5 columns (zoom_level, tile_column, tile_row, tile_data, tile_hash). -- The order is not important SELECT COUNT(*) = 5 FROM pragma_table_info('tiles_with_hash') - WHERE ((name = "zoom_level" AND type = "INTEGER") - OR (name = "tile_column" AND type = "INTEGER") - OR (name = "tile_row" AND type = "INTEGER") - OR (name = "tile_data" AND type = "BLOB") - OR (name = "tile_hash" AND type = "TEXT")) + WHERE ((name = 'zoom_level' AND type = 'INTEGER') + OR (name = 'tile_column' AND type = 'INTEGER') + OR (name = 'tile_row' AND type = 'INTEGER') + OR (name = 'tile_data' AND type = 'BLOB') + OR (name = 'tile_hash' AND type = 'TEXT')) -- - ) as is_valid; -"# + ) as is_valid;" ); Ok(sql @@ -94,26 +92,25 @@ where for<'e> &'e mut T: SqliteExecutor<'e>, { let sql = query!( - r#"SELECT ( - -- Has a "tiles" table - SELECT COUNT(*) = 1 - FROM sqlite_master - WHERE name = 'tiles' - AND type = 'table' - -- - ) AND ( - -- "tiles" table's columns and their types are as expected: - -- 4 columns (zoom_level, tile_column, tile_row, tile_data). - -- The order is not important - SELECT COUNT(*) = 4 - FROM pragma_table_info('tiles') - WHERE ((name = "zoom_level" AND type = "INTEGER") - OR (name = "tile_column" AND type = "INTEGER") - OR (name = "tile_row" AND type = "INTEGER") - OR (name = "tile_data" AND type = "BLOB")) - -- - ) as is_valid; -"# + "SELECT ( + -- Has a 'tiles' table + SELECT COUNT(*) = 1 + FROM sqlite_master + WHERE name = 'tiles' + AND type = 'table' + -- + ) AND ( + -- 'tiles' table's columns and their types are as expected: + -- 4 columns (zoom_level, tile_column, tile_row, tile_data). + -- The order is not important + SELECT COUNT(*) = 4 + FROM pragma_table_info('tiles') + WHERE ((name = 'zoom_level' AND type = 'INTEGER') + OR (name = 'tile_column' AND type = 'INTEGER') + OR (name = 'tile_row' AND type = 'INTEGER') + OR (name = 'tile_data' AND type = 'BLOB')) + -- + ) as is_valid;" ); Ok(sql @@ -123,3 +120,121 @@ where .unwrap_or_default() == 1) } + +pub async fn create_metadata_table(conn: &mut T) -> MbtResult<()> +where + for<'e> &'e mut T: SqliteExecutor<'e>, +{ + query( + "CREATE TABLE IF NOT EXISTS metadata ( + name text NOT NULL PRIMARY KEY, + value text);", + ) + .execute(&mut *conn) + .await?; + + Ok(()) +} + +pub async fn create_flat_tables(conn: &mut T) -> MbtResult<()> +where + for<'e> &'e mut T: SqliteExecutor<'e>, +{ + query( + "CREATE TABLE IF NOT EXISTS tiles ( + zoom_level integer NOT NULL, + tile_column integer NOT NULL, + tile_row integer NOT NULL, + tile_data blob, + PRIMARY KEY(zoom_level, tile_column, tile_row));", + ) + .execute(&mut *conn) + .await?; + + Ok(()) +} + +pub async fn create_flat_with_hash_tables(conn: &mut T) -> MbtResult<()> +where + for<'e> &'e mut T: SqliteExecutor<'e>, +{ + query( + "CREATE TABLE IF NOT EXISTS tiles_with_hash ( + zoom_level integer NOT NULL, + tile_column integer NOT NULL, + tile_row integer NOT NULL, + tile_data blob, + tile_hash text, + PRIMARY KEY(zoom_level, tile_column, tile_row));", + ) + .execute(&mut *conn) + .await?; + + query( + "CREATE VIEW IF NOT EXISTS tiles AS + SELECT zoom_level, tile_column, tile_row, tile_data FROM tiles_with_hash;", + ) + .execute(&mut *conn) + .await?; + + Ok(()) +} + +pub async fn create_normalized_tables(conn: &mut T) -> MbtResult<()> +where + for<'e> &'e mut T: SqliteExecutor<'e>, +{ + query( + "CREATE TABLE IF NOT EXISTS map ( + zoom_level integer NOT NULL, + tile_column integer NOT NULL, + tile_row integer NOT NULL, + tile_id text, + PRIMARY KEY(zoom_level, tile_column, tile_row));", + ) + .execute(&mut *conn) + .await?; + + query( + "CREATE TABLE IF NOT EXISTS images ( + tile_data blob, + tile_id text NOT NULL PRIMARY KEY);", + ) + .execute(&mut *conn) + .await?; + + query( + "CREATE VIEW IF NOT EXISTS tiles AS + SELECT map.zoom_level AS zoom_level, + map.tile_column AS tile_column, + map.tile_row AS tile_row, + images.tile_data AS tile_data + FROM map + JOIN images ON images.tile_id = map.tile_id;", + ) + .execute(&mut *conn) + .await?; + + Ok(()) +} + +pub async fn create_tiles_with_hash_view(conn: &mut T) -> MbtResult<()> +where + for<'e> &'e mut T: SqliteExecutor<'e>, +{ + query( + "CREATE VIEW IF NOT EXISTS tiles_with_hash AS + SELECT + map.zoom_level AS zoom_level, + map.tile_column AS tile_column, + map.tile_row AS tile_row, + images.tile_data AS tile_data, + images.tile_id AS tile_hash + FROM map + JOIN images ON images.tile_id = map.tile_id", + ) + .execute(&mut *conn) + .await?; + + Ok(()) +} diff --git a/martin-mbtiles/src/tile_copier.rs b/martin-mbtiles/src/tile_copier.rs index 8e8ced1b5..b7e18e361 100644 --- a/martin-mbtiles/src/tile_copier.rs +++ b/martin-mbtiles/src/tile_copier.rs @@ -11,6 +11,10 @@ use sqlx::{query, Connection, Row, SqliteConnection}; use crate::errors::MbtResult; use crate::mbtiles::MbtType::{Flat, FlatWithHash, Normalized}; use crate::mbtiles::{attach_hash_fn, MbtType}; +use crate::mbtiles_queries::{ + create_flat_tables, create_flat_with_hash_tables, create_metadata_table, + create_normalized_tables, create_tiles_with_hash_view, +}; use crate::{MbtError, Mbtiles}; #[derive(PartialEq, Eq, Default, Debug, Clone)] @@ -24,7 +28,7 @@ pub enum CopyDuplicateMode { #[derive(Clone, Default, PartialEq, Eq, Debug)] #[cfg_attr(feature = "cli", derive(Args))] -pub struct TileCopierOptions { +pub struct TileCopier { /// MBTiles file to read from src_file: PathBuf, /// MBTiles file to write to @@ -85,13 +89,13 @@ impl clap::builder::TypedValueParser for HashSetValueParser { } #[derive(Clone, Debug)] -struct TileCopier { +struct TileCopierInt { src_mbtiles: Mbtiles, dst_mbtiles: Mbtiles, - options: TileCopierOptions, + options: TileCopier, } -impl TileCopierOptions { +impl TileCopier { #[must_use] pub fn new(src_filepath: PathBuf, dst_filepath: PathBuf) -> Self { Self { @@ -150,13 +154,13 @@ impl TileCopierOptions { } pub async fn run(self) -> MbtResult { - TileCopier::new(self)?.run().await + TileCopierInt::new(self)?.run().await } } -impl TileCopier { - pub fn new(options: TileCopierOptions) -> MbtResult { - Ok(TileCopier { +impl TileCopierInt { + pub fn new(options: TileCopier) -> MbtResult { + Ok(TileCopierInt { src_mbtiles: Mbtiles::new(&options.src_file)?, dst_mbtiles: Mbtiles::new(&options.dst_file)?, options, @@ -164,7 +168,8 @@ impl TileCopier { } pub async fn run(self) -> MbtResult { - let src_type = open_and_detect_type(&self.src_mbtiles).await?; + // src file connection is not needed after this point, as it will be attached to the dst file + let src_type = self.src_mbtiles.open_and_detect_type().await?; let mut conn = SqliteConnection::connect_with( &SqliteConnectOptions::new() @@ -189,7 +194,7 @@ impl TileCopier { return Err(MbtError::NonEmptyTargetFile(self.options.dst_file)); } else { let dst_type = self.dst_mbtiles.detect_type(&mut conn).await?; - attach_source_db(&mut conn, self.src_mbtiles.filepath()).await?; + self.src_mbtiles.attach_to(&mut conn, "sourceDb").await?; dst_type }; @@ -198,11 +203,8 @@ impl TileCopier { let (select_from, query_args) = { let select_from = if let Some(diff_file) = &self.options.diff_with_file { let diff_with_mbtiles = Mbtiles::new(diff_file)?; - let diff_type = open_and_detect_type(&diff_with_mbtiles).await?; - let path = diff_with_mbtiles.filepath(); - query!("ATTACH DATABASE ? AS newDb", path) - .execute(&mut conn) - .await?; + let diff_type = diff_with_mbtiles.open_and_detect_type().await?; + diff_with_mbtiles.attach_to(&mut conn, "newDb").await?; Self::get_select_from_with_diff(dst_type, diff_type) } else { Self::get_select_from(dst_type, src_type).to_string() @@ -213,34 +215,40 @@ impl TileCopier { (format!("{select_from} {options_sql}"), query_args) }; - let handle = conn.lock_handle().await?.as_raw_handle().as_ptr(); - let rusqlite_conn = unsafe { rusqlite::Connection::from_handle(handle) }?; - match dst_type { - Flat => rusqlite_conn.execute( - &format!("INSERT {on_dupl} INTO tiles {select_from} {sql_cond}"), - params_from_iter(query_args), - )?, - FlatWithHash => rusqlite_conn.execute( - &format!("INSERT {on_dupl} INTO tiles_with_hash {select_from} {sql_cond}"), - params_from_iter(query_args), - )?, - Normalized => { - rusqlite_conn.execute( - &format!( - "INSERT {on_dupl} INTO map (zoom_level, tile_column, tile_row, tile_id) + { + // Make sure not to execute any other queries while the handle is locked + let mut handle_lock = conn.lock_handle().await?; + let handle = handle_lock.as_raw_handle().as_ptr(); + + // SAFETY: this is safe as long as handle_lock is valid + let rusqlite_conn = unsafe { rusqlite::Connection::from_handle(handle) }?; + match dst_type { + Flat => rusqlite_conn.execute( + &format!("INSERT {on_dupl} INTO tiles {select_from} {sql_cond}"), + params_from_iter(query_args), + )?, + FlatWithHash => rusqlite_conn.execute( + &format!("INSERT {on_dupl} INTO tiles_with_hash {select_from} {sql_cond}"), + params_from_iter(query_args), + )?, + Normalized => { + rusqlite_conn.execute( + &format!( + "INSERT {on_dupl} INTO map (zoom_level, tile_column, tile_row, tile_id) SELECT zoom_level, tile_column, tile_row, hash as tile_id FROM ({select_from} {sql_cond})" - ), - params_from_iter(&query_args), - )?; - rusqlite_conn.execute( - &format!( - "INSERT OR IGNORE INTO images SELECT tile_data, hash FROM ({select_from})" - ), - params_from_iter(query_args), - )? - } - }; + ), + params_from_iter(&query_args), + )?; + rusqlite_conn.execute( + &format!( + "INSERT OR IGNORE INTO images SELECT tile_data, hash FROM ({select_from})" + ), + params_from_iter(query_args), + )? + } + }; + } if !self.options.skip_agg_tiles_hash { self.dst_mbtiles.update_agg_tiles_hash(&mut conn).await?; @@ -258,7 +266,7 @@ impl TileCopier { query!("PRAGMA page_size = 512").execute(&mut *conn).await?; query!("VACUUM").execute(&mut *conn).await?; - attach_source_db(&mut *conn, self.src_mbtiles.filepath()).await?; + self.src_mbtiles.attach_to(&mut *conn, "sourceDb").await?; if src == dst { // DB objects must be created in a specific order: tables, views, triggers, indexes. @@ -281,27 +289,17 @@ impl TileCopier { query(row.get(0)).execute(&mut *conn).await?; } } else { + create_metadata_table(&mut *conn).await?; match dst { - Flat => self.create_flat_tables(&mut *conn).await?, - FlatWithHash => self.create_flat_with_hash_tables(&mut *conn).await?, - Normalized => self.create_normalized_tables(&mut *conn).await?, + Flat => create_flat_tables(&mut *conn).await?, + FlatWithHash => create_flat_with_hash_tables(&mut *conn).await?, + Normalized => create_normalized_tables(&mut *conn).await?, }; }; if dst == Normalized { - query( - "CREATE VIEW tiles_with_hash AS - SELECT - map.zoom_level AS zoom_level, - map.tile_column AS tile_column, - map.tile_row AS tile_row, - images.tile_data AS tile_data, - images.tile_id AS tile_hash - FROM map - JOIN images ON images.tile_id = map.tile_id", - ) - .execute(&mut *conn) - .await?; + // Some normalized mbtiles files might not have this view, so even if src == dst, it might not exist + create_tiles_with_hash_view(&mut *conn).await?; } query("INSERT INTO metadata SELECT * FROM sourceDb.metadata") @@ -311,64 +309,13 @@ impl TileCopier { Ok(()) } - async fn create_flat_tables(&self, conn: &mut SqliteConnection) -> MbtResult<()> { - for statement in &[ - "CREATE TABLE metadata (name text NOT NULL PRIMARY KEY, value text);", - "CREATE TABLE tiles ( - zoom_level integer NOT NULL, - tile_column integer NOT NULL, - tile_row integer NOT NULL, - tile_data blob, - PRIMARY KEY(zoom_level, tile_column, tile_row));", - ] { - query(statement).execute(&mut *conn).await?; - } - Ok(()) - } - - async fn create_flat_with_hash_tables(&self, conn: &mut SqliteConnection) -> MbtResult<()> { - for statement in &[ - "CREATE TABLE metadata (name text NOT NULL PRIMARY KEY, value text);", - "CREATE TABLE tiles_with_hash ( - zoom_level integer NOT NULL, - tile_column integer NOT NULL, - tile_row integer NOT NULL, - tile_data blob, - tile_hash text, - PRIMARY KEY(zoom_level, tile_column, tile_row));", - "CREATE VIEW tiles AS - SELECT zoom_level, tile_column, tile_row, tile_data FROM tiles_with_hash;", - ] { - query(statement).execute(&mut *conn).await?; - } - Ok(()) - } - - async fn create_normalized_tables(&self, conn: &mut SqliteConnection) -> MbtResult<()> { - for statement in &[ - "CREATE TABLE metadata (name text NOT NULL PRIMARY KEY, value text);", - "CREATE TABLE map ( - zoom_level integer NOT NULL, - tile_column integer NOT NULL, - tile_row integer NOT NULL, - tile_id text, - PRIMARY KEY(zoom_level, tile_column, tile_row));", - "CREATE TABLE images (tile_data blob, tile_id text NOT NULL PRIMARY KEY);", - "CREATE VIEW tiles AS - SELECT map.zoom_level AS zoom_level, map.tile_column AS tile_column, map.tile_row AS tile_row, images.tile_data AS tile_data - FROM map - JOIN images ON images.tile_id = map.tile_id;"] { - query(statement).execute(&mut *conn).await?; - } - Ok(()) - } - - fn get_on_duplicate_sql(&self, mbttype: MbtType) -> (String, String) { + /// Returns (ON DUPLICATE SQL, WHERE condition SQL) + fn get_on_duplicate_sql(&self, dst_type: MbtType) -> (String, String) { match &self.options.on_duplicate { CopyDuplicateMode::Override => ("OR REPLACE".to_string(), String::new()), CopyDuplicateMode::Ignore => ("OR IGNORE".to_string(), String::new()), CopyDuplicateMode::Abort => ("OR ABORT".to_string(), { - let (main_table, tile_identifier) = match mbttype { + let (main_table, tile_identifier) = match dst_type { Flat => ("tiles", "tile_data"), FlatWithHash => ("tiles_with_hash", "tile_data"), Normalized => ("map", "tile_id"), @@ -418,12 +365,12 @@ impl TileCopier { fn get_select_from(dst_type: MbtType, src_type: MbtType) -> &'static str { if dst_type == Flat { - "SELECT * FROM sourceDb.tiles WHERE TRUE " + "SELECT * FROM sourceDb.tiles WHERE TRUE" } else { match src_type { - Flat => "SELECT zoom_level, tile_column, tile_row, tile_data, hex(md5(tile_data)) as hash FROM sourceDb.tiles WHERE TRUE ", - FlatWithHash => "SELECT zoom_level, tile_column, tile_row, tile_data, tile_hash AS hash FROM sourceDb.tiles_with_hash WHERE TRUE ", - Normalized => "SELECT zoom_level, tile_column, tile_row, tile_data, map.tile_id AS hash FROM sourceDb.map JOIN sourceDb.images ON sourceDb.map.tile_id = sourceDb.images.tile_id WHERE TRUE " + Flat => "SELECT zoom_level, tile_column, tile_row, tile_data, hex(md5(tile_data)) as hash FROM sourceDb.tiles WHERE TRUE", + FlatWithHash => "SELECT zoom_level, tile_column, tile_row, tile_data, tile_hash AS hash FROM sourceDb.tiles_with_hash WHERE TRUE", + Normalized => "SELECT zoom_level, tile_column, tile_row, tile_data, map.tile_id AS hash FROM sourceDb.map JOIN sourceDb.images ON sourceDb.map.tile_id = sourceDb.images.tile_id WHERE TRUE" } } } @@ -459,34 +406,15 @@ impl TileCopier { } } -async fn attach_source_db(conn: &mut SqliteConnection, path: &str) -> MbtResult<()> { - query!("ATTACH DATABASE ? AS sourceDb", path) - .execute(&mut *conn) - .await?; - Ok(()) -} - -async fn open_and_detect_type(mbtiles: &Mbtiles) -> MbtResult { - let opt = SqliteConnectOptions::new() - .read_only(true) - .filename(mbtiles.filepath()); - let mut conn = SqliteConnection::connect_with(&opt).await?; - mbtiles.detect_type(&mut conn).await -} - pub async fn apply_mbtiles_diff(src_file: PathBuf, diff_file: PathBuf) -> MbtResult<()> { let src_mbtiles = Mbtiles::new(src_file)?; let diff_mbtiles = Mbtiles::new(diff_file)?; - - let src_type = open_and_detect_type(&src_mbtiles).await?; - let diff_type = open_and_detect_type(&diff_mbtiles).await?; + let diff_type = diff_mbtiles.open_and_detect_type().await?; let mut conn = src_mbtiles.open_with_hashes(false).await?; - let path = diff_mbtiles.filepath(); - query!("ATTACH DATABASE ? AS diffDb", path) - .execute(&mut conn) - .await?; + diff_mbtiles.attach_to(&mut conn, "diffDb").await?; + let src_type = src_mbtiles.detect_type(&mut conn).await?; let select_from = if src_type == Flat { "SELECT zoom_level, tile_column, tile_row, tile_data FROM diffDb.tiles" } else { @@ -534,20 +462,6 @@ mod tests { use super::*; - async fn attach_other_db(conn: &mut SqliteConnection, path: &str) -> MbtResult<()> { - query!("ATTACH DATABASE ? AS otherDb", path) - .execute(&mut *conn) - .await?; - Ok(()) - } - - async fn attach_src_db(conn: &mut SqliteConnection, path: &str) -> MbtResult<()> { - query!("ATTACH DATABASE ? AS srcDb", path) - .execute(&mut *conn) - .await?; - Ok(()) - } - async fn get_one(conn: &mut SqliteConnection, sql: &str) -> T where for<'r> T: Decode<'r, Sqlite> + Type, @@ -561,15 +475,19 @@ mod tests { dst_type: Option, expected_dst_type: MbtType, ) -> MbtResult<()> { - let mut dst_conn = TileCopierOptions::new(src_filepath.clone(), dst_filepath.clone()) + let mut dst_conn = TileCopier::new(src_filepath.clone(), dst_filepath.clone()) .dst_type(dst_type) .run() .await?; - attach_src_db(&mut dst_conn, src_filepath.to_str().unwrap()).await?; + Mbtiles::new(src_filepath)? + .attach_to(&mut dst_conn, "srcDb") + .await?; assert_eq!( - open_and_detect_type(&Mbtiles::new(dst_filepath)?).await?, + Mbtiles::new(dst_filepath)? + .detect_type(&mut dst_conn) + .await?, expected_dst_type ); @@ -584,7 +502,7 @@ mod tests { } async fn verify_copy_with_zoom_filter( - opts: TileCopierOptions, + opts: TileCopier, expected_zoom_levels: u8, ) -> MbtResult<()> { let mut dst_conn = opts.run().await?; @@ -678,7 +596,7 @@ mod tests { async fn copy_with_min_max_zoom() -> MbtResult<()> { let src = PathBuf::from("../tests/fixtures/mbtiles/world_cities.mbtiles"); let dst = PathBuf::from("file:copy_with_min_max_zoom_mem_db?mode=memory&cache=shared"); - let opt = TileCopierOptions::new(src, dst) + let opt = TileCopier::new(src, dst) .min_zoom(Some(2)) .max_zoom(Some(4)); verify_copy_with_zoom_filter(opt, 3).await @@ -688,7 +606,7 @@ mod tests { async fn copy_with_zoom_levels() -> MbtResult<()> { let src = PathBuf::from("../tests/fixtures/mbtiles/world_cities.mbtiles"); let dst = PathBuf::from("file:copy_with_zoom_levels_mem_db?mode=memory&cache=shared"); - let opt = TileCopierOptions::new(src, dst) + let opt = TileCopier::new(src, dst) .min_zoom(Some(2)) .max_zoom(Some(4)) .zoom_levels(vec![1, 6]); @@ -703,8 +621,7 @@ mod tests { let diff_file = PathBuf::from("../tests/fixtures/mbtiles/geography-class-jpg-modified.mbtiles"); - let copy_opts = - TileCopierOptions::new(src.clone(), dst.clone()).diff_with_file(diff_file.clone()); + let copy_opts = TileCopier::new(src.clone(), dst.clone()).diff_with_file(diff_file.clone()); let mut dst_conn = copy_opts.run().await?; @@ -752,9 +669,7 @@ mod tests { "file:ignore_dst_type_when_copy_to_existing_mem_db?mode=memory&cache=shared", ); - let _dst_conn = TileCopierOptions::new(dst_file.clone(), dst.clone()) - .run() - .await?; + let _dst_conn = TileCopier::new(dst_file.clone(), dst.clone()).run().await?; verify_copy_all(src_file, dst, Some(Normalized), Flat).await } @@ -765,7 +680,7 @@ mod tests { let dst = PathBuf::from("../tests/fixtures/mbtiles/world_cities.mbtiles"); let copy_opts = - TileCopierOptions::new(src.clone(), dst.clone()).on_duplicate(CopyDuplicateMode::Abort); + TileCopier::new(src.clone(), dst.clone()).on_duplicate(CopyDuplicateMode::Abort); assert!(matches!( copy_opts.run().await.unwrap_err(), @@ -782,16 +697,14 @@ mod tests { let dst = PathBuf::from("file:copy_to_existing_override_mode_mem_db?mode=memory&cache=shared"); - let _dst_conn = TileCopierOptions::new(dst_file.clone(), dst.clone()) - .run() - .await?; + let _dst_conn = TileCopier::new(dst_file.clone(), dst.clone()).run().await?; - let mut dst_conn = TileCopierOptions::new(src_file.clone(), dst.clone()) - .run() - .await?; + let mut dst_conn = TileCopier::new(src_file.clone(), dst.clone()).run().await?; // Verify the tiles in the destination file is a superset of the tiles in the source file - attach_other_db(&mut dst_conn, src_file.to_str().unwrap()).await?; + Mbtiles::new(src_file)? + .attach_to(&mut dst_conn, "otherDb") + .await?; assert!( query("SELECT * FROM otherDb.tiles EXCEPT SELECT * FROM tiles;") .fetch_optional(&mut dst_conn) @@ -811,21 +724,19 @@ mod tests { let dst = PathBuf::from("file:copy_to_existing_ignore_mode_mem_db?mode=memory&cache=shared"); - let _dst_conn = TileCopierOptions::new(dst_file.clone(), dst.clone()) - .run() - .await?; + let _dst_conn = TileCopier::new(dst_file.clone(), dst.clone()).run().await?; - let mut dst_conn = TileCopierOptions::new(src_file.clone(), dst.clone()) + let mut dst_conn = TileCopier::new(src_file.clone(), dst.clone()) .on_duplicate(CopyDuplicateMode::Ignore) .run() .await?; // Verify the tiles in the destination file are the same as those in the source file except for those with duplicate (zoom_level, tile_column, tile_row) - attach_src_db(&mut dst_conn, src_file.to_str().unwrap()).await?; - - let path = dst_file.to_str().unwrap(); - query!("ATTACH DATABASE ? AS originalDb", path) - .execute(&mut dst_conn) + Mbtiles::new(src_file)? + .attach_to(&mut dst_conn, "srcDb") + .await?; + Mbtiles::new(dst_file)? + .attach_to(&mut dst_conn, "originalDb") .await?; // Create a temporary table with all the tiles in the original database and @@ -839,8 +750,7 @@ mod tests { FULL OUTER JOIN srcDb.tiles as t2 ON t1.zoom_level = t2.zoom_level AND t1.tile_column = t2.tile_column AND t1.tile_row = t2.tile_row") .execute(&mut dst_conn) - .await - ?; + .await?; // Ensure all entries in expected_tiles are in tiles and vice versa assert!(query( @@ -861,17 +771,16 @@ mod tests { let src_file = PathBuf::from("../tests/fixtures/mbtiles/world_cities.mbtiles"); let src = PathBuf::from("file:apply_flat_diff_file_mem_db?mode=memory&cache=shared"); - let mut src_conn = TileCopierOptions::new(src_file.clone(), src.clone()) - .run() - .await?; + let mut src_conn = TileCopier::new(src_file.clone(), src.clone()).run().await?; // Apply diff to the src data in in-memory DB let diff_file = PathBuf::from("../tests/fixtures/mbtiles/world_cities_diff.mbtiles"); apply_mbtiles_diff(src, diff_file).await?; // Verify the data is the same as the file the diff was generated from - let path = "../tests/fixtures/mbtiles/world_cities_modified.mbtiles"; - attach_other_db(&mut src_conn, path).await?; + Mbtiles::new("../tests/fixtures/mbtiles/world_cities_modified.mbtiles")? + .attach_to(&mut src_conn, "otherDb") + .await?; assert!( query("SELECT * FROM tiles EXCEPT SELECT * FROM otherDb.tiles;") @@ -889,17 +798,16 @@ mod tests { let src_file = PathBuf::from("../tests/fixtures/mbtiles/geography-class-jpg.mbtiles"); let src = PathBuf::from("file:apply_normalized_diff_file_mem_db?mode=memory&cache=shared"); - let mut src_conn = TileCopierOptions::new(src_file.clone(), src.clone()) - .run() - .await?; + let mut src_conn = TileCopier::new(src_file.clone(), src.clone()).run().await?; // Apply diff to the src data in in-memory DB let diff_file = PathBuf::from("../tests/fixtures/mbtiles/geography-class-jpg-diff.mbtiles"); apply_mbtiles_diff(src, diff_file).await?; // Verify the data is the same as the file the diff was generated from - let path = "../tests/fixtures/mbtiles/geography-class-jpg-modified.mbtiles"; - attach_other_db(&mut src_conn, path).await?; + Mbtiles::new("../tests/fixtures/mbtiles/geography-class-jpg-modified.mbtiles")? + .attach_to(&mut src_conn, "otherDb") + .await?; assert!( query("SELECT * FROM tiles EXCEPT SELECT * FROM otherDb.tiles;") From 0cf57a6a6adf930c60c10dab6ca982e60dd1f81c Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Sun, 1 Oct 2023 23:29:56 -0400 Subject: [PATCH 021/108] bump versions --- Cargo.lock | 12 ++++++------ martin-tile-utils/Cargo.toml | 2 +- martin/Cargo.toml | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 207615c35..cd358dae1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1003,9 +1003,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" +checksum = "add4f07d43996f76ef320709726a556a9d4f965d9410d8d0271132d2f8293480" dependencies = [ "errno-dragonfly", "libc", @@ -1735,7 +1735,7 @@ checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "martin" -version = "0.9.0" +version = "0.9.1" dependencies = [ "actix", "actix-cors", @@ -1802,7 +1802,7 @@ dependencies = [ [[package]] name = "martin-tile-utils" -version = "0.1.2" +version = "0.1.3" [[package]] name = "md-5" @@ -1816,9 +1816,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.6.3" +version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" [[package]] name = "memmap2" diff --git a/martin-tile-utils/Cargo.toml b/martin-tile-utils/Cargo.toml index a1fdd0d6d..d4f713298 100644 --- a/martin-tile-utils/Cargo.toml +++ b/martin-tile-utils/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "martin-tile-utils" -version = "0.1.2" +version = "0.1.3" authors = ["Yuri Astrakhan ", "MapLibre contributors"] description = "Utilites to help with map tile processing, such as type and compression detection. Used by the MapLibre's Martin tile server." keywords = ["maps", "tiles", "mvt", "tileserver"] diff --git a/martin/Cargo.toml b/martin/Cargo.toml index 62119dc29..8e0d30213 100644 --- a/martin/Cargo.toml +++ b/martin/Cargo.toml @@ -2,7 +2,7 @@ name = "martin" # Make sure to update /home/nyurik/dev/rust/martin/homebrew-formula/martin.rb version # Once the release is published with the hash -version = "0.9.0" +version = "0.9.1" authors = ["Stepan Kuzmin ", "Yuri Astrakhan ", "MapLibre contributors"] description = "Blazing fast and lightweight tile server with PostGIS, MBTiles, and PMTiles support" keywords = ["maps", "tiles", "mbtiles", "pmtiles", "postgis"] From 800228332d011819497007fe3ac0cba1c9412e1d Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Mon, 2 Oct 2023 12:57:54 -0400 Subject: [PATCH 022/108] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 35527cc63..25a601d8b 100755 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ You can download martin from [GitHub releases page](https://github.com/maplibre/ | Platform | AMD-64 | ARM-64 | |----------|----------------------------------------------------------------------------------------------|-------------------------------------| -| Linux | [.tar.gz][rl-linux-x64] (gnu), [.tar.gz][rl-linux-x64-musl] (musl), [.deb][rl-linux-x64-deb] | [.tar.gz][rl-linux-a64-musl] (musl) | +| Linux | [.tar.gz][rl-linux-x64] (gnu)
[.tar.gz][rl-linux-x64-musl] (musl)
[.deb][rl-linux-x64-deb] | [.tar.gz][rl-linux-a64-musl] (musl) | | macOS | [.tar.gz][rl-macos-x64] | [.tar.gz][rl-macos-a64] | | Windows | [.zip][rl-win64-zip] | | From cc6b8fd9c8f42a6724b130a77bd88bde825e3676 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Mon, 2 Oct 2023 17:42:12 -0400 Subject: [PATCH 023/108] rename mbtiles files --- martin-mbtiles/src/bin/main.rs | 20 +++---- .../src/{tile_copier.rs => copier.rs} | 57 ++++++++++++------- martin-mbtiles/src/lib.rs | 10 ++-- martin-mbtiles/src/mbtiles.rs | 2 +- .../src/{mbtiles_pool.rs => pool.rs} | 0 .../src/{mbtiles_queries.rs => queries.rs} | 0 6 files changed, 51 insertions(+), 38 deletions(-) rename martin-mbtiles/src/{tile_copier.rs => copier.rs} (95%) rename martin-mbtiles/src/{mbtiles_pool.rs => pool.rs} (100%) rename martin-mbtiles/src/{mbtiles_queries.rs => queries.rs} (100%) diff --git a/martin-mbtiles/src/bin/main.rs b/martin-mbtiles/src/bin/main.rs index 96f7d53b9..c587428d1 100644 --- a/martin-mbtiles/src/bin/main.rs +++ b/martin-mbtiles/src/bin/main.rs @@ -2,7 +2,7 @@ use std::path::{Path, PathBuf}; use clap::{Parser, Subcommand}; use log::{error, LevelFilter}; -use martin_mbtiles::{apply_mbtiles_diff, IntegrityCheckType, MbtResult, Mbtiles, TileCopier}; +use martin_mbtiles::{apply_mbtiles_diff, IntegrityCheckType, MbtResult, Mbtiles, MbtilesCopier}; #[derive(Parser, PartialEq, Eq, Debug)] #[command( @@ -46,7 +46,7 @@ enum Commands { }, /// Copy tiles from one mbtiles file to another. #[command(name = "copy")] - Copy(TileCopier), + Copy(MbtilesCopier), /// Apply diff file generated from 'copy' command #[command(name = "apply-diff")] ApplyDiff { @@ -163,7 +163,7 @@ mod tests { use clap::error::ErrorKind; use clap::Parser; - use martin_mbtiles::{CopyDuplicateMode, TileCopier}; + use martin_mbtiles::{CopyDuplicateMode, MbtilesCopier}; use crate::Commands::{ApplyDiff, Copy, MetaGetValue, MetaSetValue, Validate}; use crate::{Args, IntegrityCheckType}; @@ -184,7 +184,7 @@ mod tests { Args::parse_from(["mbtiles", "copy", "src_file", "dst_file"]), Args { verbose: false, - command: Copy(TileCopier::new( + command: Copy(MbtilesCopier::new( PathBuf::from("src_file"), PathBuf::from("dst_file") )) @@ -208,7 +208,7 @@ mod tests { Args { verbose: false, command: Copy( - TileCopier::new(PathBuf::from("src_file"), PathBuf::from("dst_file")) + MbtilesCopier::new(PathBuf::from("src_file"), PathBuf::from("dst_file")) .min_zoom(Some(1)) .max_zoom(Some(100)) ) @@ -268,7 +268,7 @@ mod tests { Args { verbose: false, command: Copy( - TileCopier::new(PathBuf::from("src_file"), PathBuf::from("dst_file")) + MbtilesCopier::new(PathBuf::from("src_file"), PathBuf::from("dst_file")) .zoom_levels(vec![1, 3, 7]) ) } @@ -289,7 +289,7 @@ mod tests { Args { verbose: false, command: Copy( - TileCopier::new(PathBuf::from("src_file"), PathBuf::from("dst_file")) + MbtilesCopier::new(PathBuf::from("src_file"), PathBuf::from("dst_file")) .diff_with_file(PathBuf::from("no_file")) ) } @@ -310,7 +310,7 @@ mod tests { Args { verbose: false, command: Copy( - TileCopier::new(PathBuf::from("src_file"), PathBuf::from("dst_file")) + MbtilesCopier::new(PathBuf::from("src_file"), PathBuf::from("dst_file")) .on_duplicate(CopyDuplicateMode::Override) ) } @@ -331,7 +331,7 @@ mod tests { Args { verbose: false, command: Copy( - TileCopier::new(PathBuf::from("src_file"), PathBuf::from("dst_file")) + MbtilesCopier::new(PathBuf::from("src_file"), PathBuf::from("dst_file")) .on_duplicate(CopyDuplicateMode::Ignore) ) } @@ -352,7 +352,7 @@ mod tests { Args { verbose: false, command: Copy( - TileCopier::new(PathBuf::from("src_file"), PathBuf::from("dst_file")) + MbtilesCopier::new(PathBuf::from("src_file"), PathBuf::from("dst_file")) .on_duplicate(CopyDuplicateMode::Abort) ) } diff --git a/martin-mbtiles/src/tile_copier.rs b/martin-mbtiles/src/copier.rs similarity index 95% rename from martin-mbtiles/src/tile_copier.rs rename to martin-mbtiles/src/copier.rs index b7e18e361..418f4c908 100644 --- a/martin-mbtiles/src/tile_copier.rs +++ b/martin-mbtiles/src/copier.rs @@ -11,7 +11,7 @@ use sqlx::{query, Connection, Row, SqliteConnection}; use crate::errors::MbtResult; use crate::mbtiles::MbtType::{Flat, FlatWithHash, Normalized}; use crate::mbtiles::{attach_hash_fn, MbtType}; -use crate::mbtiles_queries::{ +use crate::queries::{ create_flat_tables, create_flat_with_hash_tables, create_metadata_table, create_normalized_tables, create_tiles_with_hash_view, }; @@ -28,7 +28,7 @@ pub enum CopyDuplicateMode { #[derive(Clone, Default, PartialEq, Eq, Debug)] #[cfg_attr(feature = "cli", derive(Args))] -pub struct TileCopier { +pub struct MbtilesCopier { /// MBTiles file to read from src_file: PathBuf, /// MBTiles file to write to @@ -89,13 +89,13 @@ impl clap::builder::TypedValueParser for HashSetValueParser { } #[derive(Clone, Debug)] -struct TileCopierInt { +struct MbtileCopierInt { src_mbtiles: Mbtiles, dst_mbtiles: Mbtiles, - options: TileCopier, + options: MbtilesCopier, } -impl TileCopier { +impl MbtilesCopier { #[must_use] pub fn new(src_filepath: PathBuf, dst_filepath: PathBuf) -> Self { Self { @@ -154,13 +154,13 @@ impl TileCopier { } pub async fn run(self) -> MbtResult { - TileCopierInt::new(self)?.run().await + MbtileCopierInt::new(self)?.run().await } } -impl TileCopierInt { - pub fn new(options: TileCopier) -> MbtResult { - Ok(TileCopierInt { +impl MbtileCopierInt { + pub fn new(options: MbtilesCopier) -> MbtResult { + Ok(MbtileCopierInt { src_mbtiles: Mbtiles::new(&options.src_file)?, dst_mbtiles: Mbtiles::new(&options.dst_file)?, options, @@ -475,7 +475,7 @@ mod tests { dst_type: Option, expected_dst_type: MbtType, ) -> MbtResult<()> { - let mut dst_conn = TileCopier::new(src_filepath.clone(), dst_filepath.clone()) + let mut dst_conn = MbtilesCopier::new(src_filepath.clone(), dst_filepath.clone()) .dst_type(dst_type) .run() .await?; @@ -502,7 +502,7 @@ mod tests { } async fn verify_copy_with_zoom_filter( - opts: TileCopier, + opts: MbtilesCopier, expected_zoom_levels: u8, ) -> MbtResult<()> { let mut dst_conn = opts.run().await?; @@ -596,7 +596,7 @@ mod tests { async fn copy_with_min_max_zoom() -> MbtResult<()> { let src = PathBuf::from("../tests/fixtures/mbtiles/world_cities.mbtiles"); let dst = PathBuf::from("file:copy_with_min_max_zoom_mem_db?mode=memory&cache=shared"); - let opt = TileCopier::new(src, dst) + let opt = MbtilesCopier::new(src, dst) .min_zoom(Some(2)) .max_zoom(Some(4)); verify_copy_with_zoom_filter(opt, 3).await @@ -606,7 +606,7 @@ mod tests { async fn copy_with_zoom_levels() -> MbtResult<()> { let src = PathBuf::from("../tests/fixtures/mbtiles/world_cities.mbtiles"); let dst = PathBuf::from("file:copy_with_zoom_levels_mem_db?mode=memory&cache=shared"); - let opt = TileCopier::new(src, dst) + let opt = MbtilesCopier::new(src, dst) .min_zoom(Some(2)) .max_zoom(Some(4)) .zoom_levels(vec![1, 6]); @@ -621,7 +621,8 @@ mod tests { let diff_file = PathBuf::from("../tests/fixtures/mbtiles/geography-class-jpg-modified.mbtiles"); - let copy_opts = TileCopier::new(src.clone(), dst.clone()).diff_with_file(diff_file.clone()); + let copy_opts = + MbtilesCopier::new(src.clone(), dst.clone()).diff_with_file(diff_file.clone()); let mut dst_conn = copy_opts.run().await?; @@ -669,7 +670,9 @@ mod tests { "file:ignore_dst_type_when_copy_to_existing_mem_db?mode=memory&cache=shared", ); - let _dst_conn = TileCopier::new(dst_file.clone(), dst.clone()).run().await?; + let _dst_conn = MbtilesCopier::new(dst_file.clone(), dst.clone()) + .run() + .await?; verify_copy_all(src_file, dst, Some(Normalized), Flat).await } @@ -680,7 +683,7 @@ mod tests { let dst = PathBuf::from("../tests/fixtures/mbtiles/world_cities.mbtiles"); let copy_opts = - TileCopier::new(src.clone(), dst.clone()).on_duplicate(CopyDuplicateMode::Abort); + MbtilesCopier::new(src.clone(), dst.clone()).on_duplicate(CopyDuplicateMode::Abort); assert!(matches!( copy_opts.run().await.unwrap_err(), @@ -697,9 +700,13 @@ mod tests { let dst = PathBuf::from("file:copy_to_existing_override_mode_mem_db?mode=memory&cache=shared"); - let _dst_conn = TileCopier::new(dst_file.clone(), dst.clone()).run().await?; + let _dst_conn = MbtilesCopier::new(dst_file.clone(), dst.clone()) + .run() + .await?; - let mut dst_conn = TileCopier::new(src_file.clone(), dst.clone()).run().await?; + let mut dst_conn = MbtilesCopier::new(src_file.clone(), dst.clone()) + .run() + .await?; // Verify the tiles in the destination file is a superset of the tiles in the source file Mbtiles::new(src_file)? @@ -724,9 +731,11 @@ mod tests { let dst = PathBuf::from("file:copy_to_existing_ignore_mode_mem_db?mode=memory&cache=shared"); - let _dst_conn = TileCopier::new(dst_file.clone(), dst.clone()).run().await?; + let _dst_conn = MbtilesCopier::new(dst_file.clone(), dst.clone()) + .run() + .await?; - let mut dst_conn = TileCopier::new(src_file.clone(), dst.clone()) + let mut dst_conn = MbtilesCopier::new(src_file.clone(), dst.clone()) .on_duplicate(CopyDuplicateMode::Ignore) .run() .await?; @@ -771,7 +780,9 @@ mod tests { let src_file = PathBuf::from("../tests/fixtures/mbtiles/world_cities.mbtiles"); let src = PathBuf::from("file:apply_flat_diff_file_mem_db?mode=memory&cache=shared"); - let mut src_conn = TileCopier::new(src_file.clone(), src.clone()).run().await?; + let mut src_conn = MbtilesCopier::new(src_file.clone(), src.clone()) + .run() + .await?; // Apply diff to the src data in in-memory DB let diff_file = PathBuf::from("../tests/fixtures/mbtiles/world_cities_diff.mbtiles"); @@ -798,7 +809,9 @@ mod tests { let src_file = PathBuf::from("../tests/fixtures/mbtiles/geography-class-jpg.mbtiles"); let src = PathBuf::from("file:apply_normalized_diff_file_mem_db?mode=memory&cache=shared"); - let mut src_conn = TileCopier::new(src_file.clone(), src.clone()).run().await?; + let mut src_conn = MbtilesCopier::new(src_file.clone(), src.clone()) + .run() + .await?; // Apply diff to the src data in in-memory DB let diff_file = PathBuf::from("../tests/fixtures/mbtiles/geography-class-jpg-diff.mbtiles"); diff --git a/martin-mbtiles/src/lib.rs b/martin-mbtiles/src/lib.rs index c88a8439e..66ce9d40f 100644 --- a/martin-mbtiles/src/lib.rs +++ b/martin-mbtiles/src/lib.rs @@ -6,10 +6,10 @@ pub use errors::{MbtError, MbtResult}; mod mbtiles; pub use mbtiles::{IntegrityCheckType, Mbtiles, Metadata}; -mod mbtiles_pool; -pub use mbtiles_pool::MbtilesPool; +mod pool; +pub use pool::MbtilesPool; -mod tile_copier; -pub use tile_copier::{apply_mbtiles_diff, CopyDuplicateMode, TileCopier}; +mod copier; +pub use copier::{apply_mbtiles_diff, CopyDuplicateMode, MbtilesCopier}; -mod mbtiles_queries; +mod queries; diff --git a/martin-mbtiles/src/mbtiles.rs b/martin-mbtiles/src/mbtiles.rs index c6f69892b..6c28acfc3 100644 --- a/martin-mbtiles/src/mbtiles.rs +++ b/martin-mbtiles/src/mbtiles.rs @@ -20,7 +20,7 @@ use sqlx::{query, Connection as _, Row, SqliteConnection, SqliteExecutor}; use tilejson::{tilejson, Bounds, Center, TileJSON}; use crate::errors::{MbtError, MbtResult}; -use crate::mbtiles_queries::{ +use crate::queries::{ is_flat_tables_type, is_flat_with_hash_tables_type, is_normalized_tables_type, }; use crate::MbtError::{ diff --git a/martin-mbtiles/src/mbtiles_pool.rs b/martin-mbtiles/src/pool.rs similarity index 100% rename from martin-mbtiles/src/mbtiles_pool.rs rename to martin-mbtiles/src/pool.rs diff --git a/martin-mbtiles/src/mbtiles_queries.rs b/martin-mbtiles/src/queries.rs similarity index 100% rename from martin-mbtiles/src/mbtiles_queries.rs rename to martin-mbtiles/src/queries.rs From 0459d3f748d8dca40c84c0ef7b97613f798adf92 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Tue, 3 Oct 2023 21:20:41 -0400 Subject: [PATCH 024/108] add encoding=UTF8 pragma, refactoring (#919) --- Cargo.toml | 4 +- justfile | 2 +- ...05f2a7999788167f41c685af3ca6f5a1359f4.json | 12 +++ martin-mbtiles/src/bin/main.rs | 20 ++-- martin-mbtiles/src/copier.rs | 102 ++++++++---------- martin-mbtiles/src/lib.rs | 2 +- martin-mbtiles/src/mbtiles.rs | 28 +++-- martin-mbtiles/src/queries.rs | 32 +++--- 8 files changed, 112 insertions(+), 90 deletions(-) create mode 100644 martin-mbtiles/.sqlx/query-428a035a55a07cbb9daac42c3ab05f2a7999788167f41c685af3ca6f5a1359f4.json diff --git a/Cargo.toml b/Cargo.toml index 173ab1b41..3f806e0b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,6 @@ tilejson = "0.3" tokio = { version = "1.32.0", features = ["macros"] } tokio-postgres-rustls = "0.10" -[profile.dev.package.sqlx-macros] +[profile.dev.package] # See https://github.com/launchbadge/sqlx#compile-time-verification -opt-level = 3 +sqlx-macros.opt-level = 3 diff --git a/justfile b/justfile index b5484a2e5..b60808eb2 100644 --- a/justfile +++ b/justfile @@ -134,7 +134,7 @@ test-int: clean-test install-sqlx # Run integration tests and save its output as the new expected output bless: start clean-test rm -rf tests/temp - cargo test --features bless-tests + cargo test -p martin --features bless-tests tests/test.sh rm -rf tests/expected mv tests/output tests/expected diff --git a/martin-mbtiles/.sqlx/query-428a035a55a07cbb9daac42c3ab05f2a7999788167f41c685af3ca6f5a1359f4.json b/martin-mbtiles/.sqlx/query-428a035a55a07cbb9daac42c3ab05f2a7999788167f41c685af3ca6f5a1359f4.json new file mode 100644 index 000000000..83f5d8a66 --- /dev/null +++ b/martin-mbtiles/.sqlx/query-428a035a55a07cbb9daac42c3ab05f2a7999788167f41c685af3ca6f5a1359f4.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "PRAGMA encoding = 'UTF-8'", + "describe": { + "columns": [], + "parameters": { + "Right": 0 + }, + "nullable": [] + }, + "hash": "428a035a55a07cbb9daac42c3ab05f2a7999788167f41c685af3ca6f5a1359f4" +} diff --git a/martin-mbtiles/src/bin/main.rs b/martin-mbtiles/src/bin/main.rs index c587428d1..a555794d0 100644 --- a/martin-mbtiles/src/bin/main.rs +++ b/martin-mbtiles/src/bin/main.rs @@ -2,7 +2,7 @@ use std::path::{Path, PathBuf}; use clap::{Parser, Subcommand}; use log::{error, LevelFilter}; -use martin_mbtiles::{apply_mbtiles_diff, IntegrityCheckType, MbtResult, Mbtiles, MbtilesCopier}; +use martin_mbtiles::{apply_diff, IntegrityCheckType, MbtResult, Mbtiles, MbtilesCopier}; #[derive(Parser, PartialEq, Eq, Debug)] #[command( @@ -104,14 +104,14 @@ async fn main_int() -> anyhow::Result<()> { src_file, diff_file, } => { - apply_mbtiles_diff(src_file, diff_file).await?; + apply_diff(src_file, diff_file).await?; } Commands::Validate { file, integrity_check, update_agg_tiles_hash, } => { - validate_mbtiles(file.as_path(), integrity_check, update_agg_tiles_hash).await?; + validate(file.as_path(), integrity_check, update_agg_tiles_hash).await?; } } @@ -120,7 +120,7 @@ async fn main_int() -> anyhow::Result<()> { async fn meta_print_all(file: &Path) -> anyhow::Result<()> { let mbt = Mbtiles::new(file)?; - let mut conn = mbt.open_with_hashes(true).await?; + let mut conn = mbt.open_readonly().await?; let metadata = mbt.get_metadata(&mut conn).await?; println!("{}", serde_yaml::to_string(&metadata)?); Ok(()) @@ -128,7 +128,7 @@ async fn meta_print_all(file: &Path) -> anyhow::Result<()> { async fn meta_get_value(file: &Path, key: &str) -> MbtResult<()> { let mbt = Mbtiles::new(file)?; - let mut conn = mbt.open_with_hashes(true).await?; + let mut conn = mbt.open_readonly().await?; if let Some(s) = mbt.get_metadata_value(&mut conn, key).await? { println!("{s}"); } @@ -137,17 +137,21 @@ async fn meta_get_value(file: &Path, key: &str) -> MbtResult<()> { async fn meta_set_value(file: &Path, key: &str, value: Option) -> MbtResult<()> { let mbt = Mbtiles::new(file)?; - let mut conn = mbt.open_with_hashes(false).await?; + let mut conn = mbt.open().await?; mbt.set_metadata_value(&mut conn, key, value).await } -async fn validate_mbtiles( +async fn validate( file: &Path, check_type: IntegrityCheckType, update_agg_tiles_hash: bool, ) -> MbtResult<()> { let mbt = Mbtiles::new(file)?; - let mut conn = mbt.open_with_hashes(!update_agg_tiles_hash).await?; + let mut conn = if update_agg_tiles_hash { + mbt.open().await? + } else { + mbt.open_readonly().await? + }; mbt.check_integrity(&mut conn, check_type).await?; mbt.check_each_tile_hash(&mut conn).await?; if update_agg_tiles_hash { diff --git a/martin-mbtiles/src/copier.rs b/martin-mbtiles/src/copier.rs index 418f4c908..ba1a6a9bd 100644 --- a/martin-mbtiles/src/copier.rs +++ b/martin-mbtiles/src/copier.rs @@ -6,14 +6,14 @@ use clap::{builder::ValueParser, error::ErrorKind, Args, ValueEnum}; use sqlite_hashes::rusqlite; use sqlite_hashes::rusqlite::params_from_iter; use sqlx::sqlite::SqliteConnectOptions; -use sqlx::{query, Connection, Row, SqliteConnection}; +use sqlx::{query, Connection, Executor as _, Row, SqliteConnection}; use crate::errors::MbtResult; use crate::mbtiles::MbtType::{Flat, FlatWithHash, Normalized}; use crate::mbtiles::{attach_hash_fn, MbtType}; use crate::queries::{ - create_flat_tables, create_flat_with_hash_tables, create_metadata_table, - create_normalized_tables, create_tiles_with_hash_view, + create_flat_tables, create_flat_with_hash_tables, create_normalized_tables, + create_tiles_with_hash_view, }; use crate::{MbtError, Mbtiles}; @@ -187,8 +187,7 @@ impl MbtileCopierInt { let dst_type = if is_empty { let dst_type = self.options.dst_type.unwrap_or(src_type); - self.create_new_mbtiles(&mut conn, src_type, dst_type) - .await?; + self.copy_to_new(&mut conn, src_type, dst_type).await?; dst_type } else if self.options.diff_with_file.is_some() { return Err(MbtError::NonEmptyTargetFile(self.options.dst_file)); @@ -257,39 +256,41 @@ impl MbtileCopierInt { Ok(conn) } - async fn create_new_mbtiles( + async fn copy_to_new( &self, conn: &mut SqliteConnection, src: MbtType, dst: MbtType, ) -> MbtResult<()> { query!("PRAGMA page_size = 512").execute(&mut *conn).await?; + query!("PRAGMA encoding = 'UTF-8'") + .execute(&mut *conn) + .await?; query!("VACUUM").execute(&mut *conn).await?; self.src_mbtiles.attach_to(&mut *conn, "sourceDb").await?; if src == dst { // DB objects must be created in a specific order: tables, views, triggers, indexes. - let sql_objects = query( - "SELECT sql - FROM sourceDb.sqlite_schema - WHERE tbl_name IN ('metadata', 'tiles', 'map', 'images', 'tiles_with_hash') - AND type IN ('table', 'view', 'trigger', 'index') - ORDER BY CASE - WHEN type = 'table' THEN 1 - WHEN type = 'view' THEN 2 - WHEN type = 'trigger' THEN 3 - WHEN type = 'index' THEN 4 - ELSE 5 END", - ) - .fetch_all(&mut *conn) - .await?; + let sql_objects = conn + .fetch_all( + "SELECT sql + FROM sourceDb.sqlite_schema + WHERE tbl_name IN ('metadata', 'tiles', 'map', 'images', 'tiles_with_hash') + AND type IN ('table', 'view', 'trigger', 'index') + ORDER BY CASE + WHEN type = 'table' THEN 1 + WHEN type = 'view' THEN 2 + WHEN type = 'trigger' THEN 3 + WHEN type = 'index' THEN 4 + ELSE 5 END", + ) + .await?; for row in sql_objects { query(row.get(0)).execute(&mut *conn).await?; } } else { - create_metadata_table(&mut *conn).await?; match dst { Flat => create_flat_tables(&mut *conn).await?, FlatWithHash => create_flat_with_hash_tables(&mut *conn).await?, @@ -302,8 +303,7 @@ impl MbtileCopierInt { create_tiles_with_hash_view(&mut *conn).await?; } - query("INSERT INTO metadata SELECT * FROM sourceDb.metadata") - .execute(&mut *conn) + conn.execute("INSERT INTO metadata SELECT * FROM sourceDb.metadata") .await?; Ok(()) @@ -406,12 +406,12 @@ impl MbtileCopierInt { } } -pub async fn apply_mbtiles_diff(src_file: PathBuf, diff_file: PathBuf) -> MbtResult<()> { +pub async fn apply_diff(src_file: PathBuf, diff_file: PathBuf) -> MbtResult<()> { let src_mbtiles = Mbtiles::new(src_file)?; let diff_mbtiles = Mbtiles::new(diff_file)?; let diff_type = diff_mbtiles.open_and_detect_type().await?; - let mut conn = src_mbtiles.open_with_hashes(false).await?; + let mut conn = src_mbtiles.open().await?; diff_mbtiles.attach_to(&mut conn, "diffDb").await?; let src_type = src_mbtiles.detect_type(&mut conn).await?; @@ -491,12 +491,10 @@ mod tests { expected_dst_type ); - assert!( - query("SELECT * FROM srcDb.tiles EXCEPT SELECT * FROM tiles") - .fetch_optional(&mut dst_conn) - .await? - .is_none() - ); + assert!(dst_conn + .fetch_optional("SELECT * FROM srcDb.tiles EXCEPT SELECT * FROM tiles") + .await? + .is_none()); Ok(()) } @@ -626,8 +624,8 @@ mod tests { let mut dst_conn = copy_opts.run().await?; - assert!(query("SELECT 1 FROM sqlite_schema WHERE name = 'tiles';") - .fetch_optional(&mut dst_conn) + assert!(dst_conn + .fetch_optional("SELECT 1 FROM sqlite_schema WHERE name = 'tiles';") .await? .is_some()); @@ -712,12 +710,10 @@ mod tests { Mbtiles::new(src_file)? .attach_to(&mut dst_conn, "otherDb") .await?; - assert!( - query("SELECT * FROM otherDb.tiles EXCEPT SELECT * FROM tiles;") - .fetch_optional(&mut dst_conn) - .await? - .is_none() - ); + assert!(dst_conn + .fetch_optional("SELECT * FROM otherDb.tiles EXCEPT SELECT * FROM tiles;") + .await? + .is_none()); Ok(()) } @@ -750,7 +746,8 @@ mod tests { // Create a temporary table with all the tiles in the original database and // all the tiles in the source database except for those that conflict with tiles in the original database - query("CREATE TEMP TABLE expected_tiles AS + dst_conn.execute( + "CREATE TEMP TABLE expected_tiles AS SELECT COALESCE(t1.zoom_level, t2.zoom_level) as zoom_level, COALESCE(t1.tile_column, t2.zoom_level) as tile_column, COALESCE(t1.tile_row, t2.tile_row) as tile_row, @@ -758,7 +755,6 @@ mod tests { FROM originalDb.tiles as t1 FULL OUTER JOIN srcDb.tiles as t2 ON t1.zoom_level = t2.zoom_level AND t1.tile_column = t2.tile_column AND t1.tile_row = t2.tile_row") - .execute(&mut dst_conn) .await?; // Ensure all entries in expected_tiles are in tiles and vice versa @@ -786,19 +782,17 @@ mod tests { // Apply diff to the src data in in-memory DB let diff_file = PathBuf::from("../tests/fixtures/mbtiles/world_cities_diff.mbtiles"); - apply_mbtiles_diff(src, diff_file).await?; + apply_diff(src, diff_file).await?; // Verify the data is the same as the file the diff was generated from Mbtiles::new("../tests/fixtures/mbtiles/world_cities_modified.mbtiles")? .attach_to(&mut src_conn, "otherDb") .await?; - assert!( - query("SELECT * FROM tiles EXCEPT SELECT * FROM otherDb.tiles;") - .fetch_optional(&mut src_conn) - .await? - .is_none() - ); + assert!(src_conn + .fetch_optional("SELECT * FROM tiles EXCEPT SELECT * FROM otherDb.tiles;") + .await? + .is_none()); Ok(()) } @@ -815,19 +809,17 @@ mod tests { // Apply diff to the src data in in-memory DB let diff_file = PathBuf::from("../tests/fixtures/mbtiles/geography-class-jpg-diff.mbtiles"); - apply_mbtiles_diff(src, diff_file).await?; + apply_diff(src, diff_file).await?; // Verify the data is the same as the file the diff was generated from Mbtiles::new("../tests/fixtures/mbtiles/geography-class-jpg-modified.mbtiles")? .attach_to(&mut src_conn, "otherDb") .await?; - assert!( - query("SELECT * FROM tiles EXCEPT SELECT * FROM otherDb.tiles;") - .fetch_optional(&mut src_conn) - .await? - .is_none() - ); + assert!(src_conn + .fetch_optional("SELECT * FROM tiles EXCEPT SELECT * FROM otherDb.tiles;") + .await? + .is_none()); Ok(()) } diff --git a/martin-mbtiles/src/lib.rs b/martin-mbtiles/src/lib.rs index 66ce9d40f..dff5b1052 100644 --- a/martin-mbtiles/src/lib.rs +++ b/martin-mbtiles/src/lib.rs @@ -10,6 +10,6 @@ mod pool; pub use pool::MbtilesPool; mod copier; -pub use copier::{apply_mbtiles_diff, CopyDuplicateMode, MbtilesCopier}; +pub use copier::{apply_diff, CopyDuplicateMode, MbtilesCopier}; mod queries; diff --git a/martin-mbtiles/src/mbtiles.rs b/martin-mbtiles/src/mbtiles.rs index 6c28acfc3..6cedee852 100644 --- a/martin-mbtiles/src/mbtiles.rs +++ b/martin-mbtiles/src/mbtiles.rs @@ -93,11 +93,27 @@ impl Mbtiles { }) } - pub async fn open_with_hashes(&self, readonly: bool) -> MbtResult { + pub async fn open(&self) -> MbtResult { + let opt = SqliteConnectOptions::new().filename(self.filepath()); + Self::open_int(&opt).await + } + + pub async fn open_or_new(&self) -> MbtResult { + let opt = SqliteConnectOptions::new() + .filename(self.filepath()) + .create_if_missing(true); + Self::open_int(&opt).await + } + + pub async fn open_readonly(&self) -> MbtResult { let opt = SqliteConnectOptions::new() .filename(self.filepath()) - .read_only(readonly); - let mut conn = SqliteConnection::connect_with(&opt).await?; + .read_only(true); + Self::open_int(&opt).await + } + + async fn open_int(opt: &SqliteConnectOptions) -> Result { + let mut conn = SqliteConnection::connect_with(opt).await?; attach_hash_fn(&mut conn).await?; Ok(conn) } @@ -373,7 +389,7 @@ impl Mbtiles { } pub async fn open_and_detect_type(&self) -> MbtResult { - let mut conn = self.open_with_hashes(true).await?; + let mut conn = self.open_readonly().await?; self.detect_type(&mut conn).await } @@ -615,6 +631,7 @@ mod tests { use std::collections::HashMap; use martin_tile_utils::Encoding; + use sqlx::Executor as _; use tilejson::VectorLayer; use super::*; @@ -706,8 +723,7 @@ mod tests { async fn metadata_set_key() -> MbtResult<()> { let (mut conn, mbt) = open("file:metadata_set_key_mem_db?mode=memory&cache=shared").await?; - query("CREATE TABLE metadata (name text NOT NULL PRIMARY KEY, value text);") - .execute(&mut conn) + conn.execute("CREATE TABLE metadata (name text NOT NULL PRIMARY KEY, value text);") .await?; mbt.set_metadata_value(&mut conn, "bounds", Some("0.0, 0.0, 0.0, 0.0".to_string())) diff --git a/martin-mbtiles/src/queries.rs b/martin-mbtiles/src/queries.rs index 9df2731bf..2986dab69 100644 --- a/martin-mbtiles/src/queries.rs +++ b/martin-mbtiles/src/queries.rs @@ -1,4 +1,4 @@ -use sqlx::{query, SqliteExecutor}; +use sqlx::{query, Executor as _, SqliteExecutor}; use crate::errors::MbtResult; @@ -125,12 +125,11 @@ pub async fn create_metadata_table(conn: &mut T) -> MbtResult<()> where for<'e> &'e mut T: SqliteExecutor<'e>, { - query( + conn.execute( "CREATE TABLE IF NOT EXISTS metadata ( name text NOT NULL PRIMARY KEY, value text);", ) - .execute(&mut *conn) .await?; Ok(()) @@ -140,7 +139,9 @@ pub async fn create_flat_tables(conn: &mut T) -> MbtResult<()> where for<'e> &'e mut T: SqliteExecutor<'e>, { - query( + create_metadata_table(&mut *conn).await?; + + conn.execute( "CREATE TABLE IF NOT EXISTS tiles ( zoom_level integer NOT NULL, tile_column integer NOT NULL, @@ -148,7 +149,6 @@ where tile_data blob, PRIMARY KEY(zoom_level, tile_column, tile_row));", ) - .execute(&mut *conn) .await?; Ok(()) @@ -158,7 +158,9 @@ pub async fn create_flat_with_hash_tables(conn: &mut T) -> MbtResult<()> where for<'e> &'e mut T: SqliteExecutor<'e>, { - query( + create_metadata_table(&mut *conn).await?; + + conn.execute( "CREATE TABLE IF NOT EXISTS tiles_with_hash ( zoom_level integer NOT NULL, tile_column integer NOT NULL, @@ -167,14 +169,12 @@ where tile_hash text, PRIMARY KEY(zoom_level, tile_column, tile_row));", ) - .execute(&mut *conn) .await?; - query( + conn.execute( "CREATE VIEW IF NOT EXISTS tiles AS SELECT zoom_level, tile_column, tile_row, tile_data FROM tiles_with_hash;", ) - .execute(&mut *conn) .await?; Ok(()) @@ -184,7 +184,9 @@ pub async fn create_normalized_tables(conn: &mut T) -> MbtResult<()> where for<'e> &'e mut T: SqliteExecutor<'e>, { - query( + create_metadata_table(&mut *conn).await?; + + conn.execute( "CREATE TABLE IF NOT EXISTS map ( zoom_level integer NOT NULL, tile_column integer NOT NULL, @@ -192,18 +194,16 @@ where tile_id text, PRIMARY KEY(zoom_level, tile_column, tile_row));", ) - .execute(&mut *conn) .await?; - query( + conn.execute( "CREATE TABLE IF NOT EXISTS images ( tile_data blob, tile_id text NOT NULL PRIMARY KEY);", ) - .execute(&mut *conn) .await?; - query( + conn.execute( "CREATE VIEW IF NOT EXISTS tiles AS SELECT map.zoom_level AS zoom_level, map.tile_column AS tile_column, @@ -212,7 +212,6 @@ where FROM map JOIN images ON images.tile_id = map.tile_id;", ) - .execute(&mut *conn) .await?; Ok(()) @@ -222,7 +221,7 @@ pub async fn create_tiles_with_hash_view(conn: &mut T) -> MbtResult<()> where for<'e> &'e mut T: SqliteExecutor<'e>, { - query( + conn.execute( "CREATE VIEW IF NOT EXISTS tiles_with_hash AS SELECT map.zoom_level AS zoom_level, @@ -233,7 +232,6 @@ where FROM map JOIN images ON images.tile_id = map.tile_id", ) - .execute(&mut *conn) .await?; Ok(()) From afc9ef2866f4904077d4bd0ff5eb5ef78739bd20 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 7 Oct 2023 01:22:12 -0400 Subject: [PATCH 025/108] chore(deps): Bump postcss from 8.4.27 to 8.4.31 in /demo/frontend (#920) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [postcss](https://github.com/postcss/postcss) from 8.4.27 to 8.4.31.
Release notes

Sourced from postcss's releases.

8.4.31

  • Fixed \r parsing to fix CVE-2023-44270.

8.4.30

8.4.29

8.4.28

  • Fixed Root.source.end for better source map (by @​romainmenke).
  • Fixed Result.root types when process() has no parser.
Changelog

Sourced from postcss's changelog.

8.4.31

  • Fixed \r parsing to fix CVE-2023-44270.

8.4.30

  • Improved source map performance (by Romain Menke).

8.4.29

  • Fixed Node#source.offset (by Ido Rosenthal).
  • Fixed docs (by Christian Oliff).

8.4.28

  • Fixed Root.source.end for better source map (by Romain Menke).
  • Fixed Result.root types when process() has no parser.
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=postcss&package-manager=npm_and_yarn&previous-version=8.4.27&new-version=8.4.31)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/maplibre/martin/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- demo/frontend/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/demo/frontend/yarn.lock b/demo/frontend/yarn.lock index 5cfc47b61..c3b4ffff1 100644 --- a/demo/frontend/yarn.lock +++ b/demo/frontend/yarn.lock @@ -3593,9 +3593,9 @@ postcss-value-parser@^4.0.2: integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== postcss@^8.4.27: - version "8.4.27" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.27.tgz#234d7e4b72e34ba5a92c29636734349e0d9c3057" - integrity sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ== + version "8.4.31" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d" + integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ== dependencies: nanoid "^3.3.6" picocolors "^1.0.0" From 7d5e2ae4bd858c4041dd6f76dc6015eb938de7f2 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Sat, 7 Oct 2023 14:46:54 -0400 Subject: [PATCH 026/108] upgrade deps --- Cargo.lock | 60 +++++++++++++++++++++++++++--------------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cd358dae1..b96fb4d0d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -105,7 +105,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -219,7 +219,7 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -230,7 +230,7 @@ checksum = "7c7db3d5a9718568e4cf4a537cfd7070e6e6ff7481510d0237fb529ac850f6d3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -401,7 +401,7 @@ checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -412,7 +412,7 @@ checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -538,9 +538,9 @@ checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" [[package]] name = "byteorder" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" @@ -643,7 +643,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -850,7 +850,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37e366bff8cd32dd8754b0991fb66b279dc48f598c3a18914852a6673deef583" dependencies = [ "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -979,7 +979,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -1257,7 +1257,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -1655,9 +1655,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.148" +version = "0.2.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" +checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" [[package]] name = "libdeflate-sys" @@ -1679,9 +1679,9 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] name = "libsqlite3-sys" @@ -2063,7 +2063,7 @@ dependencies = [ "regex", "regex-syntax", "structmeta", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -2305,9 +2305,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.67" +version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" +checksum = "5b1106fec09662ec6dd98ccac0f81cef56984d0b49f75c92d8cbad76e20c005c" dependencies = [ "unicode-ident", ] @@ -2530,9 +2530,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.15" +version = "0.38.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2f9da0cbd88f9f09e7814e388301c8414c51c62aa6ce1e4b5c551d49d96e531" +checksum = "f25469e9ae0f3d0047ca8b93fc56843f38e6774f0914a107ff8b41be8be8e0b7" dependencies = [ "bitflags 2.4.0", "errno", @@ -2686,7 +2686,7 @@ checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -3140,7 +3140,7 @@ dependencies = [ "proc-macro2", "quote", "structmeta-derive", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -3151,7 +3151,7 @@ checksum = "a60bcaff7397072dca0017d1db428e30d5002e00b6847703e2e42005c95fbe00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -3195,9 +3195,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.37" +version = "2.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" +checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" dependencies = [ "proc-macro2", "quote", @@ -3255,7 +3255,7 @@ checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -3385,7 +3385,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -3484,7 +3484,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -3749,7 +3749,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", "wasm-bindgen-shared", ] @@ -3771,7 +3771,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", "wasm-bindgen-backend", "wasm-bindgen-shared", ] From eaee56cd6044b630dac72dd7ef68653e267f8803 Mon Sep 17 00:00:00 2001 From: Lucas Date: Sun, 8 Oct 2023 17:02:13 +0800 Subject: [PATCH 027/108] Rename homebrew formula subdirctory (#923) Rename hombrew formula directory from `homebrew-formula` to `HomebrewFormula` based on the [homebrew doc](https://docs.brew.sh/How-to-Create-and-Maintain-a-Tap#creating-a-tap). --- .github/workflows/ci.yml | 4 ++-- .github/workflows/grcov.yml | 4 ++-- {homebrew-formula => HomebrewFormula}/martin.rb | 0 martin/Cargo.toml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) rename {homebrew-formula => HomebrewFormula}/martin.rb (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e094397e8..a67acba26 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,14 +7,14 @@ on: - '**.md' - 'demo/**' - 'docs/**' - - 'homebrew-formula/**' + - 'HomebrewFormula/**' pull_request: branches: [ main ] paths-ignore: - '**.md' - 'demo/**' - 'docs/**' - - 'homebrew-formula/**' + - 'HomebrewFormula/**' release: types: [ published ] workflow_dispatch: diff --git a/.github/workflows/grcov.yml b/.github/workflows/grcov.yml index b4a61db40..4d7f22b44 100644 --- a/.github/workflows/grcov.yml +++ b/.github/workflows/grcov.yml @@ -7,14 +7,14 @@ on: - '**.md' - 'demo/**' - 'docs/**' - - 'homebrew-formula/**' + - 'HomebrewFormula/**' pull_request: branches: [ main ] paths-ignore: - '**.md' - 'demo/**' - 'docs/**' - - 'homebrew-formula/**' + - 'HomebrewFormula/**' workflow_dispatch: jobs: diff --git a/homebrew-formula/martin.rb b/HomebrewFormula/martin.rb similarity index 100% rename from homebrew-formula/martin.rb rename to HomebrewFormula/martin.rb diff --git a/martin/Cargo.toml b/martin/Cargo.toml index 8e0d30213..727317cea 100644 --- a/martin/Cargo.toml +++ b/martin/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "martin" -# Make sure to update /home/nyurik/dev/rust/martin/homebrew-formula/martin.rb version +# Make sure to update /home/nyurik/dev/rust/martin/HomebrewFormula/martin.rb version # Once the release is published with the hash version = "0.9.1" authors = ["Stepan Kuzmin ", "Yuri Astrakhan ", "MapLibre contributors"] From 321a969a39c765cfe936ab9b3d4cdb982d02659e Mon Sep 17 00:00:00 2001 From: Pat Sier Date: Sun, 8 Oct 2023 16:13:00 -0400 Subject: [PATCH 028/108] Log a warning if PG startup takes too long (#924) Adds a warning message using the suggested implementation in #810. Thanks for the recommended approach in the ticket! This one seemed straightforward enough that local tests would do, which I ran by adding a delay to `instantiate_tables`. --------- Co-authored-by: Yuri Astrakhan --- martin/src/pg/config.rs | 14 ++++++++++++-- martin/src/pg/configurator.rs | 8 ++++++++ martin/src/utils/utilities.rs | 18 ++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/martin/src/pg/config.rs b/martin/src/pg/config.rs index c84aa01b3..1a46bb90c 100644 --- a/martin/src/pg/config.rs +++ b/martin/src/pg/config.rs @@ -1,4 +1,7 @@ +use std::time::Duration; + use futures::future::try_join; +use log::warn; use serde::{Deserialize, Serialize}; use tilejson::TileJSON; @@ -8,7 +11,7 @@ use crate::pg::config_table::TableInfoSources; use crate::pg::configurator::PgBuilder; use crate::pg::Result; use crate::source::Sources; -use crate::utils::{sorted_opt_map, BoolOrObject, IdResolver, OneOrMany}; +use crate::utils::{on_slow, sorted_opt_map, BoolOrObject, IdResolver, OneOrMany}; pub trait PgInfo { fn format_id(&self) -> String; @@ -110,8 +113,15 @@ impl PgConfig { pub async fn resolve(&mut self, id_resolver: IdResolver) -> crate::Result { let pg = PgBuilder::new(self, id_resolver).await?; + let inst_tables = on_slow(pg.instantiate_tables(), Duration::from_secs(5), || { + if pg.disable_bounds() { + warn!("Discovering tables in PostgreSQL database '{}' is taking too long. Bounds calculation is already disabled. You may need to tune your database.", pg.get_id()); + } else { + warn!("Discovering tables in PostgreSQL database '{}' is taking too long. Make sure your table geo columns have a GIS index, or use --disable-bounds CLI/config to skip bbox calculation.", pg.get_id()); + } + }); let ((mut tables, tbl_info), (funcs, func_info)) = - try_join(pg.instantiate_tables(), pg.instantiate_functions()).await?; + try_join(inst_tables, pg.instantiate_functions()).await?; self.tables = Some(tbl_info); self.functions = Some(func_info); diff --git a/martin/src/pg/configurator.rs b/martin/src/pg/configurator.rs index d5fbf77d0..5acdeb65b 100755 --- a/martin/src/pg/configurator.rs +++ b/martin/src/pg/configurator.rs @@ -63,6 +63,14 @@ impl PgBuilder { }) } + pub fn disable_bounds(&self) -> bool { + self.disable_bounds + } + + pub fn get_id(&self) -> &str { + self.pool.get_id() + } + // FIXME: this function has gotten too long due to the new formatting rules, need to be refactored #[allow(clippy::too_many_lines)] pub async fn instantiate_tables(&self) -> Result<(Sources, TableInfoSources)> { diff --git a/martin/src/utils/utilities.rs b/martin/src/utils/utilities.rs index c7c67d2ed..6635990bb 100644 --- a/martin/src/utils/utilities.rs +++ b/martin/src/utils/utilities.rs @@ -1,9 +1,13 @@ use std::collections::{BTreeMap, HashMap}; +use std::future::Future; use std::io::{Read as _, Write as _}; +use std::time::Duration; use flate2::read::GzDecoder; use flate2::write::GzEncoder; +use futures::pin_mut; use serde::{Deserialize, Serialize, Serializer}; +use tokio::time::timeout; #[must_use] pub fn is_valid_zoom(zoom: u8, minzoom: Option, maxzoom: Option) -> bool { @@ -58,3 +62,17 @@ pub fn encode_brotli(data: &[u8]) -> Result, std::io::Error> { encoder.write_all(data)?; Ok(encoder.into_inner()) } + +pub async fn on_slow( + future: impl Future, + duration: Duration, + fn_on_slow: S, +) -> T { + pin_mut!(future); + if let Ok(result) = timeout(duration, &mut future).await { + result + } else { + fn_on_slow(); + future.await + } +} From ecd051a492433401157f8548af46ad67631631f5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Oct 2023 03:03:11 +0000 Subject: [PATCH 029/108] chore(deps): Bump semver from 1.0.19 to 1.0.20 (#926) Bumps [semver](https://github.com/dtolnay/semver) from 1.0.19 to 1.0.20.
Release notes

Sourced from semver's releases.

1.0.20

  • Add a method for comparing versions by precedence (#305)
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=semver&package-manager=cargo&previous-version=1.0.19&new-version=1.0.20)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b96fb4d0d..a610e1ead 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2665,9 +2665,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad977052201c6de01a8ef2aa3378c4bd23217a056337d1d6da40468d267a4fb0" +checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" [[package]] name = "serde" From f4bb5c7c27ab3966d93aedadacd2eac087890dcd Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Tue, 10 Oct 2023 02:41:56 -0400 Subject: [PATCH 030/108] bump versions --- Cargo.lock | 225 ++++++++------------------------------ Cargo.toml | 8 +- martin/src/sprites/mod.rs | 10 +- 3 files changed, 52 insertions(+), 191 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a610e1ead..ceef8b118 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -262,9 +262,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea5d730647d4fadd988536d06fecce94b7b4f2a7efdae548f1cf4b63205518ab" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" dependencies = [ "memchr", ] @@ -362,21 +362,6 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" -[[package]] -name = "assert_fs" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f070617a68e5c2ed5d06ee8dd620ee18fb72b99f6c094bed34cf8ab07c875b48" -dependencies = [ - "anstyle", - "doc-comment", - "globwalk", - "predicates", - "predicates-core", - "predicates-tree", - "tempfile", -] - [[package]] name = "async-compression" version = "0.4.3" @@ -514,16 +499,6 @@ dependencies = [ "alloc-stdlib", ] -[[package]] -name = "bstr" -version = "1.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c2f7349907b712260e64b0afe2f84692af14a454be26187d9df565c7f69266a" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "bumpalo" version = "3.14.0" @@ -922,12 +897,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "difflib" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" - [[package]] name = "digest" version = "0.10.7" @@ -940,12 +909,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "doc-comment" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" - [[package]] name = "dotenvy" version = "0.15.7" @@ -1003,25 +966,14 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "add4f07d43996f76ef320709726a556a9d4f965d9410d8d0271132d2f8293480" +checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" dependencies = [ - "errno-dragonfly", "libc", "windows-sys", ] -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", - "libc", -] - [[package]] name = "etcetera" version = "0.8.0" @@ -1039,12 +991,6 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" -[[package]] -name = "exitcode" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de853764b47027c2e862a995c34978ffa63c1501f2e15f987ba11bd4f9bba193" - [[package]] name = "fallible-iterator" version = "0.2.0" @@ -1327,30 +1273,6 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" -[[package]] -name = "globset" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "759c97c1e17c55525b57192c06a267cda0ac5210b222d6b82189a2338fa1c13d" -dependencies = [ - "aho-corasick", - "bstr", - "fnv", - "log", - "regex", -] - -[[package]] -name = "globwalk" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" -dependencies = [ - "bitflags 1.3.2", - "ignore", - "walkdir", -] - [[package]] name = "h2" version = "0.3.21" @@ -1494,23 +1416,6 @@ dependencies = [ "unicode-normalization", ] -[[package]] -name = "ignore" -version = "0.4.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbe7873dab538a9a44ad79ede1faf5f30d49f9a5c883ddbab48bce81b64b7492" -dependencies = [ - "globset", - "lazy_static", - "log", - "memchr", - "regex", - "same-file", - "thread_local", - "walkdir", - "winapi-util", -] - [[package]] name = "image" version = "0.24.7" @@ -1619,9 +1524,9 @@ dependencies = [ [[package]] name = "json-patch" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f7765dccf8c39c3a470fc694efe322969d791e713ca46bc7b5c506886157572" +checksum = "55ff1e1486799e3f64129f8ccad108b38290df9cd7015cd31bed17239f0789d6" dependencies = [ "serde", "serde_json", @@ -1696,9 +1601,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.8" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3852614a3bd9ca9804678ba6be5e3b8ce76dfc902cae004e3e0c44051b6e88db" +checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" [[package]] name = "local-channel" @@ -1951,9 +1856,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ "autocfg", "libm", @@ -2061,7 +1966,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "regex-syntax", + "regex-syntax 0.7.5", "structmeta", "syn 2.0.38", ] @@ -2275,39 +2180,11 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" -[[package]] -name = "predicates" -version = "3.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dfc28575c2e3f19cb3c73b93af36460ae898d426eba6fc15b9bd2a5220758a0" -dependencies = [ - "anstyle", - "difflib", - "itertools 0.11.0", - "predicates-core", -] - -[[package]] -name = "predicates-core" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" - -[[package]] -name = "predicates-tree" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" -dependencies = [ - "predicates-core", - "termtree", -] - [[package]] name = "proc-macro2" -version = "1.0.68" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b1106fec09662ec6dd98ccac0f81cef56984d0b49f75c92d8cbad76e20c005c" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" dependencies = [ "unicode-ident", ] @@ -2394,25 +2271,25 @@ dependencies = [ [[package]] name = "regex" -version = "1.9.6" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebee201405406dbf528b8b672104ae6d6d63e6d118cb10e4d51abbc7b58044ff" +checksum = "d119d7c7ca818f8a53c300863d4f87566aac09943aef5b355bb83969dae75d87" dependencies = [ "aho-corasick", "memchr", "regex-automata", - "regex-syntax", + "regex-syntax 0.8.0", ] [[package]] name = "regex-automata" -version = "0.3.9" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9" +checksum = "465c6fc0621e4abc4187a2bda0937bfd4f722c2730b29562e19689ea796c9a4b" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.8.0", ] [[package]] @@ -2421,11 +2298,17 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" +[[package]] +name = "regex-syntax" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3cbb081b9784b07cceb8824c8583f86db4814d172ab043f3c23f7dc600bf83d" + [[package]] name = "resvg" -version = "0.34.1" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0e3d65cea36eefb28a020edb6e66341764e00cd4b426e0c1f0599b1adaa78f5" +checksum = "b6554f47c38eca56827eea7f285c2a3018b4e12e0e195cc105833c008be338f1" dependencies = [ "gif", "jpeg-decoder", @@ -2530,9 +2413,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.17" +version = "0.38.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f25469e9ae0f3d0047ca8b93fc56843f38e6774f0914a107ff8b41be8be8e0b7" +checksum = "5a74ee2d7c2581cd139b42447d7d9389b889bdaad3a73f1ebb16f2a3237bb19c" dependencies = [ "bitflags 2.4.0", "errno", @@ -2870,18 +2753,14 @@ dependencies = [ [[package]] name = "spreet" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357c86676a23af570a68dbc1f59d2d6442d12ac7c94c41b8317f30706f5ca05d" +checksum = "c73c2f90a7b1281c08144af5dc91f2e32fdc4752d764aa4ff95c224f7b51502c" dependencies = [ - "assert_fs", - "clap", "crunch", - "exitcode", "multimap", "oxipng", "png", - "rayon", "resvg", "serde", "serde_json", @@ -2900,11 +2779,13 @@ dependencies = [ [[package]] name = "sqlite-hashes" -version = "0.3.3" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd203121770e67b5f689ebf9592c88d3529193743f35630413f419be8ef1e835" +checksum = "9d7ef02a3d30492f243536808bba25455404ed91aaf91309bf55c4b036e9e8da" dependencies = [ "digest", + "hex", + "log", "md-5", "rusqlite", "sha1", @@ -3232,12 +3113,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "termtree" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" - [[package]] name = "thiserror" version = "1.0.49" @@ -3258,16 +3133,6 @@ dependencies = [ "syn 2.0.38", ] -[[package]] -name = "thread_local" -version = "1.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" -dependencies = [ - "cfg-if", - "once_cell", -] - [[package]] name = "tilejson" version = "0.3.2" @@ -3360,9 +3225,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.32.0" +version = "1.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" +checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653" dependencies = [ "backtrace", "bytes", @@ -3623,9 +3488,9 @@ dependencies = [ [[package]] name = "usvg" -version = "0.34.1" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2304b933107198a910c1f3219acb65246f2b148f862703cffd51c6e62156abe" +checksum = "14d09ddfb0d93bf84824c09336d32e42f80961a9d1680832eb24fdf249ce11e6" dependencies = [ "base64", "log", @@ -3638,9 +3503,9 @@ dependencies = [ [[package]] name = "usvg-parser" -version = "0.34.0" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12b940fea80394e3b14cb21c83fa1b8f8a41023c25929bba68bb84a76193ebed" +checksum = "d19bf93d230813599927d88557014e0908ecc3531666d47c634c6838bc8db408" dependencies = [ "data-url", "flate2", @@ -3656,9 +3521,9 @@ dependencies = [ [[package]] name = "usvg-text-layout" -version = "0.34.0" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69dfd6119f431aa7e969b4a69f9cc8b9ae37b8ae85bb26780ccfa3beaf8b71eb" +checksum = "035044604e89652c0a2959b8b356946997a52649ba6cade45928c2842376feb4" dependencies = [ "fontdb", "kurbo", @@ -3672,9 +3537,9 @@ dependencies = [ [[package]] name = "usvg-tree" -version = "0.34.0" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3185eb13b6e3d3cf1817d29612251cc308d5a7e5e6235362e67efe832435c6d9" +checksum = "7939a7e4ed21cadb5d311d6339730681c3e24c3e81d60065be80e485d3fc8b92" dependencies = [ "rctree", "strict-num", diff --git a/Cargo.toml b/Cargo.toml index 3f806e0b6..45293a24c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ flate2 = "1" futures = "0.3" indoc = "2" itertools = "0.11" -json-patch = "1.1" +json-patch = "1.2" log = "0.4" martin-mbtiles = { path = "./martin-mbtiles", version = "0.6.0", default-features = false } martin-tile-utils = { path = "./martin-tile-utils", version = "0.1.0" } @@ -46,13 +46,13 @@ semver = "1" serde = { version = "1", features = ["derive"] } serde_json = "1" serde_yaml = "0.9" -spreet = { version = "0.8", default-features = false } -sqlite-hashes = "0.3" +spreet = { version = "0.9", default-features = false } +sqlite-hashes = "0.5" sqlx = { version = "0.7", features = ["sqlite", "runtime-tokio"] } subst = { version = "0.3", features = ["yaml"] } thiserror = "1" tilejson = "0.3" -tokio = { version = "1.32.0", features = ["macros"] } +tokio = { version = "1.33.0", features = ["macros"] } tokio-postgres-rustls = "0.10" [profile.dev.package] diff --git a/martin/src/sprites/mod.rs b/martin/src/sprites/mod.rs index 519b280fb..c1b5943ed 100644 --- a/martin/src/sprites/mod.rs +++ b/martin/src/sprites/mod.rs @@ -6,9 +6,8 @@ use std::path::PathBuf; use futures::future::try_join_all; use log::{info, warn}; use spreet::fs::get_svg_input_paths; -use spreet::resvg::tiny_skia::Pixmap; use spreet::resvg::usvg::{Error as ResvgError, Options, Tree, TreeParsing}; -use spreet::sprite::{generate_pixmap_from_svg, sprite_name, Spritesheet, SpritesheetBuilder}; +use spreet::sprite::{sprite_name, Sprite, Spritesheet, SpritesheetBuilder}; use tokio::io::AsyncReadExt; use crate::file_config::{FileConfigEnum, FileError}; @@ -132,7 +131,7 @@ async fn parse_sprite( name: String, path: PathBuf, pixel_ratio: u8, -) -> Result<(String, Pixmap), SpriteError> { +) -> Result<(String, Sprite), SpriteError> { let on_err = |e| SpriteError::IoError(e, path.clone()); let mut file = tokio::fs::File::open(&path).await.map_err(on_err)?; @@ -143,10 +142,7 @@ async fn parse_sprite( let tree = Tree::from_data(&buffer, &Options::default()) .map_err(|e| SpriteError::SpriteParsingError(e, path.clone()))?; - let pixmap = generate_pixmap_from_svg(&tree, pixel_ratio) - .ok_or_else(|| SpriteError::UnableToReadSprite(path.clone()))?; - - Ok((name, pixmap)) + Ok((name, Sprite { tree, pixel_ratio })) } pub async fn get_spritesheet( From 8b34cd374c707548f9e1dccd9385aad0e804f62c Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Tue, 10 Oct 2023 11:10:17 -0400 Subject: [PATCH 031/108] Add metadat copy/apply-diff, new testing framework (#921) * Fix metadata copying * Introduce a new metadata field `agg_tiles_hash_after_apply` for diff files * Added a lot of new info and debug logging * Simplified Copying interface - not much value in having all the complex builder pattern here it seems, might as well use a simple object. ## Testing * Generate SQLite DBs in memory on the fly to validate just what we need * Use `insta` for validating DB content There is now a function `dump(connection) -> Vec` to dump the content of the entire SQLite DB into text with `serde`. At many steps through the testing, the DB content is validated with the corresponding .snap file with `insta` crate (which makes this process mega-simple, including a simple way to "bless" (update) any changes). ## Discovered bugs * Seems like normalized files do not get copied properly - they contain extras that should be removed. --- Cargo.lock | 283 ++++++++- Cargo.toml | 12 +- README.md | 8 +- docs/src/development.md | 2 +- docs/src/tools.md | 13 +- justfile | 33 +- ...37901cbe4b6421bac3cf671e86d4b5d8dc0f3.json | 20 - ...aaca18373bbdd548a8378ae7fbeed351b4b87.json | 20 + ...22ea61633c21afb45d3d2b9aeec068d72cce0.json | 20 + martin-mbtiles/Cargo.toml | 6 + martin-mbtiles/src/bin/main.rs | 103 ++-- martin-mbtiles/src/copier.rs | 548 ++++++++---------- martin-mbtiles/src/errors.rs | 8 +- martin-mbtiles/src/lib.rs | 14 +- martin-mbtiles/src/mbtiles.rs | 123 ++-- martin-mbtiles/src/patcher.rs | 189 ++++++ martin-mbtiles/src/queries.rs | 67 ++- martin-mbtiles/tests/mbtiles.rs | 409 +++++++++++++ .../mbtiles__convert@v1__z6__flat-flat.snap | 42 ++ .../mbtiles__convert@v1__z6__flat-hash.snap | 50 ++ .../mbtiles__convert@v1__z6__flat-norm.snap | 85 +++ .../mbtiles__convert@v1__z6__hash-flat.snap | 42 ++ .../mbtiles__convert@v1__z6__hash-hash.snap | 50 ++ .../mbtiles__convert@v1__z6__hash-norm.snap | 85 +++ .../mbtiles__convert@v1__z6__norm-flat.snap | 42 ++ .../mbtiles__convert@v1__z6__norm-hash.snap | 50 ++ .../mbtiles__convert@v1__z6__norm-norm.snap | 85 +++ .../mbtiles__databases@flat__dif.snap | 45 ++ .../mbtiles__databases@flat__v1-no-hash.snap | 44 ++ .../mbtiles__databases@flat__v1.snap | 45 ++ .../mbtiles__databases@flat__v2.snap | 45 ++ .../mbtiles__databases@hash__dif.snap | 53 ++ .../mbtiles__databases@hash__v1-no-hash.snap | 52 ++ .../mbtiles__databases@hash__v1.snap | 53 ++ .../mbtiles__databases@hash__v2.snap | 53 ++ .../mbtiles__databases@norm__dif.snap | 89 +++ .../mbtiles__databases@norm__v1-no-hash.snap | 88 +++ .../mbtiles__databases@norm__v1.snap | 89 +++ .../mbtiles__databases@norm__v2.snap | 90 +++ martin/src/bin/main.rs | 2 +- martin/src/pg/table_source.rs | 2 - tests/expected/mbtiles/copy_diff.txt | 3 +- tests/expected/mbtiles/copy_diff2.txt | 3 +- tests/fixtures/mbtiles/world_cities.mbties | 0 tests/pg_server_test.rs | 5 +- 45 files changed, 2674 insertions(+), 496 deletions(-) delete mode 100644 martin-mbtiles/.sqlx/query-4905d37cd3818e2fe9f65fdd20437901cbe4b6421bac3cf671e86d4b5d8dc0f3.json create mode 100644 martin-mbtiles/.sqlx/query-77b2f46851c4e991230ec6a5d33aaca18373bbdd548a8378ae7fbeed351b4b87.json create mode 100644 martin-mbtiles/.sqlx/query-85b46712c445679053e768cc98b22ea61633c21afb45d3d2b9aeec068d72cce0.json create mode 100644 martin-mbtiles/src/patcher.rs create mode 100644 martin-mbtiles/tests/mbtiles.rs create mode 100644 martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__flat-flat.snap create mode 100644 martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__flat-hash.snap create mode 100644 martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__flat-norm.snap create mode 100644 martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__hash-flat.snap create mode 100644 martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__hash-hash.snap create mode 100644 martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__hash-norm.snap create mode 100644 martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__norm-flat.snap create mode 100644 martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__norm-hash.snap create mode 100644 martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__norm-norm.snap create mode 100644 martin-mbtiles/tests/snapshots/mbtiles__databases@flat__dif.snap create mode 100644 martin-mbtiles/tests/snapshots/mbtiles__databases@flat__v1-no-hash.snap create mode 100644 martin-mbtiles/tests/snapshots/mbtiles__databases@flat__v1.snap create mode 100644 martin-mbtiles/tests/snapshots/mbtiles__databases@flat__v2.snap create mode 100644 martin-mbtiles/tests/snapshots/mbtiles__databases@hash__dif.snap create mode 100644 martin-mbtiles/tests/snapshots/mbtiles__databases@hash__v1-no-hash.snap create mode 100644 martin-mbtiles/tests/snapshots/mbtiles__databases@hash__v1.snap create mode 100644 martin-mbtiles/tests/snapshots/mbtiles__databases@hash__v2.snap create mode 100644 martin-mbtiles/tests/snapshots/mbtiles__databases@norm__dif.snap create mode 100644 martin-mbtiles/tests/snapshots/mbtiles__databases@norm__v1-no-hash.snap create mode 100644 martin-mbtiles/tests/snapshots/mbtiles__databases@norm__v1.snap create mode 100644 martin-mbtiles/tests/snapshots/mbtiles__databases@norm__v2.snap create mode 100644 tests/fixtures/mbtiles/world_cities.mbties diff --git a/Cargo.lock b/Cargo.lock index ceef8b118..06a2b0e08 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -331,7 +331,7 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" dependencies = [ - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -341,7 +341,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" dependencies = [ "anstyle", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -639,6 +639,18 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "console" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "windows-sys 0.45.0", +] + [[package]] name = "const-oid" version = "0.9.5" @@ -651,6 +663,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "cookie" version = "0.16.2" @@ -890,13 +911,19 @@ version = "0.99.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" dependencies = [ - "convert_case", + "convert_case 0.4.0", "proc-macro2", "quote", "rustc_version", "syn 1.0.109", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "digest" version = "0.10.7" @@ -924,6 +951,12 @@ dependencies = [ "serde", ] +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "encoding_rs" version = "0.8.33" @@ -933,6 +966,26 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "enum-display" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d4df33d54dd1959d177a0e2c2f4e5a8637a3054aa56861ed7e173ad2043fe2" +dependencies = [ + "enum-display-macro", +] + +[[package]] +name = "enum-display-macro" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0ce3a36047ede676eb0d2721d065beed8410cf4f113f489604d2971331cb378" +dependencies = [ + "convert_case 0.6.0", + "quote", + "syn 1.0.109", +] + [[package]] name = "enum_dispatch" version = "0.3.12" @@ -971,7 +1024,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -982,7 +1035,7 @@ checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" dependencies = [ "cfg-if", "home", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1027,7 +1080,7 @@ dependencies = [ "cfg-if", "libc", "redox_syscall", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1127,7 +1180,7 @@ dependencies = [ "async-trait", "rustix", "tokio", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1218,6 +1271,12 @@ version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +[[package]] +name = "futures-timer" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" + [[package]] name = "futures-util" version = "0.3.28" @@ -1273,6 +1332,12 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "h2" version = "0.3.21" @@ -1374,7 +1439,7 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" dependencies = [ - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1463,6 +1528,21 @@ version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" +[[package]] +name = "insta" +version = "1.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d64600be34b2fcfc267740a243fa7744441bb4947a619ac4e5bb6507f35fbfc" +dependencies = [ + "console", + "lazy_static", + "linked-hash-map", + "serde", + "similar", + "toml", + "yaml-rust", +] + [[package]] name = "is-terminal" version = "0.4.9" @@ -1471,7 +1551,7 @@ checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ "hermit-abi", "rustix", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1599,6 +1679,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.4.10" @@ -1691,10 +1777,15 @@ dependencies = [ "actix-rt", "anyhow", "clap", + "ctor", + "enum-display", "env_logger", "futures", + "insta", "log", "martin-tile-utils", + "pretty_assertions", + "rstest", "serde", "serde_json", "serde_yaml", @@ -1783,7 +1874,7 @@ dependencies = [ "libc", "log", "wasi", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1942,7 +2033,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -2180,6 +2271,16 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "pretty_assertions" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "proc-macro2" version = "1.0.69" @@ -2304,6 +2405,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3cbb081b9784b07cceb8824c8583f86db4814d172ab043f3c23f7dc600bf83d" +[[package]] +name = "relative-path" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c707298afce11da2efef2f600116fa93ffa7a032b5d7b628aa17711ec81383ca" + [[package]] name = "resvg" version = "0.35.0" @@ -2376,6 +2483,35 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rstest" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97eeab2f3c0a199bc4be135c36c924b6590b88c377d416494288c14f2db30199" +dependencies = [ + "futures", + "futures-timer", + "rstest_macros", + "rustc_version", +] + +[[package]] +name = "rstest_macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d428f8247852f894ee1be110b375111b586d4fa431f6c46e64ba5a0dcccbe605" +dependencies = [ + "cfg-if", + "glob", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn 2.0.38", + "unicode-ident", +] + [[package]] name = "rusqlite" version = "0.29.0" @@ -2421,7 +2557,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -2504,7 +2640,7 @@ version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" dependencies = [ - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -2677,6 +2813,12 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "similar" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2aeaf503862c419d66959f5d7ca015337d864e9c49485d771b732e2a20453597" + [[package]] name = "simplecss" version = "0.2.1" @@ -2723,7 +2865,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -2785,11 +2927,8 @@ checksum = "9d7ef02a3d30492f243536808bba25455404ed91aaf91309bf55c4b036e9e8da" dependencies = [ "digest", "hex", - "log", "md-5", "rusqlite", - "sha1", - "sha2", ] [[package]] @@ -3101,7 +3240,7 @@ dependencies = [ "fastrand", "redox_syscall", "rustix", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -3239,7 +3378,7 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -3328,6 +3467,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "tracing" version = "0.1.37" @@ -3704,13 +3852,37 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", ] [[package]] @@ -3719,51 +3891,93 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -3791,6 +4005,21 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" + [[package]] name = "zeroize" version = "1.6.0" diff --git a/Cargo.toml b/Cargo.toml index 45293a24c..9531ce2e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,10 +24,12 @@ clap = { version = "4", features = ["derive"] } criterion = { version = "0.5", features = ["async_futures", "async_tokio", "html_reports"] } ctor = "0.2" deadpool-postgres = "0.11" +enum-display = "0.1" env_logger = "0.10" flate2 = "1" futures = "0.3" indoc = "2" +insta = { version = "1", features = ["toml"] } itertools = "0.11" json-patch = "1.2" log = "0.4" @@ -38,7 +40,9 @@ pmtiles = { version = "0.3", features = ["mmap-async-tokio", "tilejson"] } postgis = "0.9" postgres = { version = "0.19", features = ["with-time-0_3", "with-uuid-1", "with-serde_json-1"] } postgres-protocol = "0.6" +pretty_assertions = "1" regex = "1" +rstest = "0.18" rustls = { version = "0.21", features = ["dangerous_configuration"] } rustls-native-certs = "0.6" rustls-pemfile = "1" @@ -47,7 +51,7 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" serde_yaml = "0.9" spreet = { version = "0.9", default-features = false } -sqlite-hashes = "0.5" +sqlite-hashes = { version = "0.5", default-features = false, features = ["md5", "window", "hex"] } sqlx = { version = "0.7", features = ["sqlite", "runtime-tokio"] } subst = { version = "0.3", features = ["yaml"] } thiserror = "1" @@ -58,3 +62,9 @@ tokio-postgres-rustls = "0.10" [profile.dev.package] # See https://github.com/launchbadge/sqlx#compile-time-verification sqlx-macros.opt-level = 3 +# See https://docs.rs/insta/latest/insta/#optional-faster-runs +insta.opt-level = 3 +similar.opt-level = 3 + +#[patch.crates-io] +#sqlite-hashes = { path = "/home/nyurik/dev/rust/sqlite-hashes" } diff --git a/README.md b/README.md index 25a601d8b..b66c18be9 100755 --- a/README.md +++ b/README.md @@ -23,11 +23,11 @@ _See [installation instructions](https://maplibre.org/martin/installation.html) You can download martin from [GitHub releases page](https://github.com/maplibre/martin/releases). -| Platform | AMD-64 | ARM-64 | -|----------|----------------------------------------------------------------------------------------------|-------------------------------------| +| Platform | AMD-64 | ARM-64 | +|----------|--------------------------------------------------------------------------------------------------|-------------------------------------| | Linux | [.tar.gz][rl-linux-x64] (gnu)
[.tar.gz][rl-linux-x64-musl] (musl)
[.deb][rl-linux-x64-deb] | [.tar.gz][rl-linux-a64-musl] (musl) | -| macOS | [.tar.gz][rl-macos-x64] | [.tar.gz][rl-macos-a64] | -| Windows | [.zip][rl-win64-zip] | | +| macOS | [.tar.gz][rl-macos-x64] | [.tar.gz][rl-macos-a64] | +| Windows | [.zip][rl-win64-zip] | | [rl-linux-x64]: https://github.com/maplibre/martin/releases/latest/download/martin-Linux-x86_64.tar.gz [rl-linux-x64-musl]: https://github.com/maplibre/martin/releases/latest/download/martin-Linux-x86_64-musl.tar.gz diff --git a/docs/src/development.md b/docs/src/development.md index 86891fbed..1475e6c76 100644 --- a/docs/src/development.md +++ b/docs/src/development.md @@ -54,7 +54,7 @@ Available recipes: test # Run all tests using a test database test-ssl # Run all tests using an SSL connection to a test database. Expected output won't match. test-legacy # Run all tests using the oldest supported version of the database - test-unit *ARGS # Run Rust unit and doc tests (cargo test) + test-cargo *ARGS # Run Rust unit and doc tests (cargo test) test-int # Run integration tests bless # Run integration tests and save its output as the new expected output book # Build and open mdbook documentation diff --git a/docs/src/tools.md b/docs/src/tools.md index bbb63082c..d0fded3c1 100644 --- a/docs/src/tools.md +++ b/docs/src/tools.md @@ -36,7 +36,10 @@ mbtiles copy src_file.mbtiles dst_file.mbtiles \ --min-zoom 0 --max-zoom 10 ``` -Copy command can also be used to compare two mbtiles files and generate a diff. +Copy command can also be used to compare two mbtiles files and generate a delta (diff) file. The diff file can be applied to the `src_file.mbtiles` elsewhere, to avoid copying/transmitting the entire modified dataset. The delta file will contain all tiles that are different between the two files (modifications, insertions, and deletions as `NULL` values), for both the tile and metadata tables. + +There is one exception: `agg_tiles_hash` metadata value will be renamed to `agg_tiles_hash_in_diff`, and a new `agg_tiles_hash` will be generated for the diff file itself. This is done to avoid confusion when applying the diff file to the original file, as the `agg_tiles_hash` value will be different after the diff is applied. The `apply-diff` command will automatically rename the `agg_tiles_hash_in_diff` value back to `agg_tiles_hash` when applying the diff. + ```shell mbtiles copy src_file.mbtiles diff_file.mbtiles \ --diff-with-file modified_file.mbtiles @@ -49,6 +52,9 @@ mbtiles copy normalized.mbtiles dst.mbtiles \ ``` ### apply-diff Apply the diff file generated from `copy` command above to an mbtiles file. The diff file can be applied to the `src_file.mbtiles` elsewhere, to avoid copying/transmitting the entire modified dataset. + +Note that the `agg_tiles_hash_in_diff` metadata value will be renamed to `agg_tiles_hash` when applying the diff. This is done to avoid confusion when applying the diff file to the original file, as the `agg_tiles_hash` value will be different after the diff is applied. + ```shell mbtiles apply_diff src_file.mbtiles diff_file.mbtiles ``` @@ -105,7 +111,7 @@ CREATE VIEW tiles AS SELECT zoom_level, tile_column, tile_row, tile_data FROM ti ```sql, ignore CREATE TABLE map (zoom_level INTEGER, tile_column INTEGER, tile_row INTEGER, tile_id TEXT); CREATE UNIQUE INDEX map_index ON map (zoom_level, tile_column, tile_row); -CREATE TABLE images (tile_data blob, tile_id text); +CREATE TABLE images (tile_id text, tile_data blob); CREATE UNIQUE INDEX images_id ON images (tile_id); CREATE VIEW tiles AS SELECT @@ -127,8 +133,7 @@ CREATE VIEW tiles_with_hash AS map.tile_row AS tile_row, images.tile_data AS tile_data, images.tile_id AS tile_hash - FROM map - JOIN images ON images.tile_id = map.tile_id; + FROM map LEFT JOIN images ON map.tile_id = images.tile_id; ``` **__Note:__** All `normalized` files created by the `mbtiles` tool will contain this view. diff --git a/justfile b/justfile index b60808eb2..9f7d8d106 100644 --- a/justfile +++ b/justfile @@ -6,8 +6,9 @@ export PGPORT := "5411" export DATABASE_URL := "postgres://postgres:postgres@localhost:" + PGPORT + "/db" export CARGO_TERM_COLOR := "always" -# export RUST_LOG := "debug" -# export RUST_BACKTRACE := "1" +#export RUST_LOG := "debug" +#export RUST_LOG := "sqlx::query=info,trace" +#export RUST_BACKTRACE := "1" @_default: just --list --unsorted @@ -88,10 +89,10 @@ bench-http: (cargo-install "oha") oha -z 120s http://localhost:3000/function_zxy_query/18/235085/122323 # Run all tests using a test database -test: start test-unit test-int +test: start (test-cargo "--all-targets") test-doc test-int # Run all tests using an SSL connection to a test database. Expected output won't match. -test-ssl: start-ssl test-unit clean-test +test-ssl: start-ssl (test-cargo "--all-targets") test-doc clean-test tests/test.sh # Run all tests using an SSL connection with client cert to a test database. Expected output won't match. @@ -107,16 +108,21 @@ test-ssl-cert: start-ssl-cert export PGSSLROOTCERT="$KEY_DIR/ssl-cert-snakeoil.pem" export PGSSLCERT="$KEY_DIR/ssl-cert-snakeoil.pem" export PGSSLKEY="$KEY_DIR/ssl-cert-snakeoil.key" - {{just_executable()}} test-unit clean-test + {{just_executable()}} test-cargo --all-targets + {{just_executable()}} clean-test + {{just_executable()}} test-doc tests/test.sh # Run all tests using the oldest supported version of the database -test-legacy: start-legacy test-unit test-int +test-legacy: start-legacy (test-cargo "--all-targets") test-doc test-int -# Run Rust unit and doc tests (cargo test) -test-unit *ARGS: - cargo test --all-targets {{ ARGS }} - cargo test --doc +# Run Rust unit tests (cargo test) +test-cargo *ARGS: + cargo test {{ ARGS }} + +# Run Rust doc tests +test-doc *ARGS: + cargo test --doc {{ ARGS }} # Run integration tests test-int: clean-test install-sqlx @@ -132,13 +138,18 @@ test-int: clean-test install-sqlx fi # Run integration tests and save its output as the new expected output -bless: start clean-test +bless: start clean-test bless-insta rm -rf tests/temp cargo test -p martin --features bless-tests tests/test.sh rm -rf tests/expected mv tests/output tests/expected +# Run integration tests and save its output as the new expected output +bless-insta *ARGS: (cargo-install "insta" "cargo-insta") + #rm -rf martin-mbtiles/tests/snapshots + cargo insta test --accept --unreferenced=auto -p martin-mbtiles {{ ARGS }} + # Build and open mdbook documentation book: (cargo-install "mdbook") mdbook serve docs --open --port 8321 diff --git a/martin-mbtiles/.sqlx/query-4905d37cd3818e2fe9f65fdd20437901cbe4b6421bac3cf671e86d4b5d8dc0f3.json b/martin-mbtiles/.sqlx/query-4905d37cd3818e2fe9f65fdd20437901cbe4b6421bac3cf671e86d4b5d8dc0f3.json deleted file mode 100644 index 47392647f..000000000 --- a/martin-mbtiles/.sqlx/query-4905d37cd3818e2fe9f65fdd20437901cbe4b6421bac3cf671e86d4b5d8dc0f3.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT (\n -- Has a 'tiles_with_hash' table\n SELECT COUNT(*) = 1\n FROM sqlite_master\n WHERE name = 'tiles_with_hash'\n AND type = 'table'\n --\n ) AND (\n -- 'tiles_with_hash' table's columns and their types are as expected:\n -- 5 columns (zoom_level, tile_column, tile_row, tile_data, tile_hash).\n -- The order is not important\n SELECT COUNT(*) = 5\n FROM pragma_table_info('tiles_with_hash')\n WHERE ((name = 'zoom_level' AND type = 'INTEGER')\n OR (name = 'tile_column' AND type = 'INTEGER')\n OR (name = 'tile_row' AND type = 'INTEGER')\n OR (name = 'tile_data' AND type = 'BLOB')\n OR (name = 'tile_hash' AND type = 'TEXT'))\n --\n ) as is_valid;", - "describe": { - "columns": [ - { - "name": "is_valid", - "ordinal": 0, - "type_info": "Int" - } - ], - "parameters": { - "Right": 0 - }, - "nullable": [ - null - ] - }, - "hash": "4905d37cd3818e2fe9f65fdd20437901cbe4b6421bac3cf671e86d4b5d8dc0f3" -} diff --git a/martin-mbtiles/.sqlx/query-77b2f46851c4e991230ec6a5d33aaca18373bbdd548a8378ae7fbeed351b4b87.json b/martin-mbtiles/.sqlx/query-77b2f46851c4e991230ec6a5d33aaca18373bbdd548a8378ae7fbeed351b4b87.json new file mode 100644 index 000000000..acb0b2ecc --- /dev/null +++ b/martin-mbtiles/.sqlx/query-77b2f46851c4e991230ec6a5d33aaca18373bbdd548a8378ae7fbeed351b4b87.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "SELECT (\n -- Has a 'tiles_with_hash' table\n SELECT COUNT(*) = 1\n FROM sqlite_master\n WHERE name = 'tiles_with_hash'\n AND type = 'table'\n --\n ) as is_valid;", + "describe": { + "columns": [ + { + "name": "is_valid", + "ordinal": 0, + "type_info": "Int" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + null + ] + }, + "hash": "77b2f46851c4e991230ec6a5d33aaca18373bbdd548a8378ae7fbeed351b4b87" +} diff --git a/martin-mbtiles/.sqlx/query-85b46712c445679053e768cc98b22ea61633c21afb45d3d2b9aeec068d72cce0.json b/martin-mbtiles/.sqlx/query-85b46712c445679053e768cc98b22ea61633c21afb45d3d2b9aeec068d72cce0.json new file mode 100644 index 000000000..45fdc4f2a --- /dev/null +++ b/martin-mbtiles/.sqlx/query-85b46712c445679053e768cc98b22ea61633c21afb45d3d2b9aeec068d72cce0.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "SELECT (\n -- 'tiles_with_hash' table or view columns and their types are as expected:\n -- 5 columns (zoom_level, tile_column, tile_row, tile_data, tile_hash).\n -- The order is not important\n SELECT COUNT(*) = 5\n FROM pragma_table_info('tiles_with_hash')\n WHERE ((name = 'zoom_level' AND type = 'INTEGER')\n OR (name = 'tile_column' AND type = 'INTEGER')\n OR (name = 'tile_row' AND type = 'INTEGER')\n OR (name = 'tile_data' AND type = 'BLOB')\n OR (name = 'tile_hash' AND type = 'TEXT'))\n --\n ) as is_valid;", + "describe": { + "columns": [ + { + "name": "is_valid", + "ordinal": 0, + "type_info": "Int" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + null + ] + }, + "hash": "85b46712c445679053e768cc98b22ea61633c21afb45d3d2b9aeec068d72cce0" +} diff --git a/martin-mbtiles/Cargo.toml b/martin-mbtiles/Cargo.toml index 155792850..957618f4d 100644 --- a/martin-mbtiles/Cargo.toml +++ b/martin-mbtiles/Cargo.toml @@ -14,6 +14,7 @@ default = ["cli"] cli = ["dep:anyhow", "dep:clap", "dep:env_logger", "dep:serde_yaml", "dep:tokio"] [dependencies] +enum-display.workspace = true futures.workspace = true log.workspace = true martin-tile-utils.workspace = true @@ -34,6 +35,11 @@ tokio = { workspace = true, features = ["rt-multi-thread"], optional = true } [dev-dependencies] # For testing, might as well use the same async framework as the Martin itself actix-rt.workspace = true +ctor.workspace = true +env_logger.workspace = true +insta.workspace = true +pretty_assertions.workspace = true +rstest.workspace = true [lib] path = "src/lib.rs" diff --git a/martin-mbtiles/src/bin/main.rs b/martin-mbtiles/src/bin/main.rs index a555794d0..5dd16c9b1 100644 --- a/martin-mbtiles/src/bin/main.rs +++ b/martin-mbtiles/src/bin/main.rs @@ -1,8 +1,8 @@ use std::path::{Path, PathBuf}; use clap::{Parser, Subcommand}; -use log::{error, LevelFilter}; -use martin_mbtiles::{apply_diff, IntegrityCheckType, MbtResult, Mbtiles, MbtilesCopier}; +use log::error; +use martin_mbtiles::{apply_patch, IntegrityCheckType, MbtResult, Mbtiles, MbtilesCopier}; #[derive(Parser, PartialEq, Eq, Debug)] #[command( @@ -71,8 +71,8 @@ enum Commands { #[tokio::main] async fn main() { - env_logger::builder() - .filter_level(LevelFilter::Info) + let env = env_logger::Env::default().default_filter_or("info"); + env_logger::Builder::from_env(env) .format_indent(None) .format_module_path(false) .format_target(false) @@ -95,7 +95,7 @@ async fn main_int() -> anyhow::Result<()> { meta_get_value(file.as_path(), &key).await?; } Commands::MetaSetValue { file, key, value } => { - meta_set_value(file.as_path(), &key, value).await?; + meta_set_value(file.as_path(), &key, value.as_deref()).await?; } Commands::Copy(opts) => { opts.run().await?; @@ -104,14 +104,15 @@ async fn main_int() -> anyhow::Result<()> { src_file, diff_file, } => { - apply_diff(src_file, diff_file).await?; + apply_patch(src_file, diff_file).await?; } Commands::Validate { file, integrity_check, update_agg_tiles_hash, } => { - validate(file.as_path(), integrity_check, update_agg_tiles_hash).await?; + let mbt = Mbtiles::new(file.as_path())?; + mbt.validate(integrity_check, update_agg_tiles_hash).await?; } } @@ -135,32 +136,12 @@ async fn meta_get_value(file: &Path, key: &str) -> MbtResult<()> { Ok(()) } -async fn meta_set_value(file: &Path, key: &str, value: Option) -> MbtResult<()> { +async fn meta_set_value(file: &Path, key: &str, value: Option<&str>) -> MbtResult<()> { let mbt = Mbtiles::new(file)?; let mut conn = mbt.open().await?; mbt.set_metadata_value(&mut conn, key, value).await } -async fn validate( - file: &Path, - check_type: IntegrityCheckType, - update_agg_tiles_hash: bool, -) -> MbtResult<()> { - let mbt = Mbtiles::new(file)?; - let mut conn = if update_agg_tiles_hash { - mbt.open().await? - } else { - mbt.open_readonly().await? - }; - mbt.check_integrity(&mut conn, check_type).await?; - mbt.check_each_tile_hash(&mut conn).await?; - if update_agg_tiles_hash { - mbt.update_agg_tiles_hash(&mut conn).await - } else { - mbt.check_agg_tiles_hashes(&mut conn).await - } -} - #[cfg(test)] mod tests { use std::path::PathBuf; @@ -198,24 +179,25 @@ mod tests { #[test] fn test_copy_min_max_zoom_arguments() { + let mut opt = MbtilesCopier::new(PathBuf::from("src_file"), PathBuf::from("dst_file")); + opt.min_zoom = Some(1); + opt.max_zoom = Some(100); + + let args = Args::parse_from([ + "mbtiles", + "copy", + "src_file", + "dst_file", + "--max-zoom", + "100", + "--min-zoom", + "1", + ]); assert_eq!( - Args::parse_from([ - "mbtiles", - "copy", - "src_file", - "dst_file", - "--max-zoom", - "100", - "--min-zoom", - "1" - ]), + args, Args { verbose: false, - command: Copy( - MbtilesCopier::new(PathBuf::from("src_file"), PathBuf::from("dst_file")) - .min_zoom(Some(1)) - .max_zoom(Some(100)) - ) + command: Copy(opt) } ); } @@ -260,6 +242,8 @@ mod tests { #[test] fn test_copy_zoom_levels_arguments() { + let mut opt = MbtilesCopier::new(PathBuf::from("src_file"), PathBuf::from("dst_file")); + opt.zoom_levels.extend(&[1, 3, 7]); assert_eq!( Args::parse_from([ "mbtiles", @@ -271,16 +255,15 @@ mod tests { ]), Args { verbose: false, - command: Copy( - MbtilesCopier::new(PathBuf::from("src_file"), PathBuf::from("dst_file")) - .zoom_levels(vec![1, 3, 7]) - ) + command: Copy(opt) } ); } #[test] fn test_copy_diff_with_file_arguments() { + let mut opt = MbtilesCopier::new(PathBuf::from("src_file"), PathBuf::from("dst_file")); + opt.diff_with_file = Some(PathBuf::from("no_file")); assert_eq!( Args::parse_from([ "mbtiles", @@ -292,16 +275,15 @@ mod tests { ]), Args { verbose: false, - command: Copy( - MbtilesCopier::new(PathBuf::from("src_file"), PathBuf::from("dst_file")) - .diff_with_file(PathBuf::from("no_file")) - ) + command: Copy(opt) } ); } #[test] fn test_copy_diff_with_override_copy_duplicate_mode() { + let mut opt = MbtilesCopier::new(PathBuf::from("src_file"), PathBuf::from("dst_file")); + opt.on_duplicate = CopyDuplicateMode::Override; assert_eq!( Args::parse_from([ "mbtiles", @@ -313,16 +295,15 @@ mod tests { ]), Args { verbose: false, - command: Copy( - MbtilesCopier::new(PathBuf::from("src_file"), PathBuf::from("dst_file")) - .on_duplicate(CopyDuplicateMode::Override) - ) + command: Copy(opt) } ); } #[test] fn test_copy_diff_with_ignore_copy_duplicate_mode() { + let mut opt = MbtilesCopier::new(PathBuf::from("src_file"), PathBuf::from("dst_file")); + opt.on_duplicate = CopyDuplicateMode::Ignore; assert_eq!( Args::parse_from([ "mbtiles", @@ -334,16 +315,15 @@ mod tests { ]), Args { verbose: false, - command: Copy( - MbtilesCopier::new(PathBuf::from("src_file"), PathBuf::from("dst_file")) - .on_duplicate(CopyDuplicateMode::Ignore) - ) + command: Copy(opt) } ); } #[test] fn test_copy_diff_with_abort_copy_duplicate_mode() { + let mut opt = MbtilesCopier::new(PathBuf::from("src_file"), PathBuf::from("dst_file")); + opt.on_duplicate = CopyDuplicateMode::Abort; assert_eq!( Args::parse_from([ "mbtiles", @@ -355,10 +335,7 @@ mod tests { ]), Args { verbose: false, - command: Copy( - MbtilesCopier::new(PathBuf::from("src_file"), PathBuf::from("dst_file")) - .on_duplicate(CopyDuplicateMode::Abort) - ) + command: Copy(opt) } ); } diff --git a/martin-mbtiles/src/copier.rs b/martin-mbtiles/src/copier.rs index ba1a6a9bd..3af7626a3 100644 --- a/martin-mbtiles/src/copier.rs +++ b/martin-mbtiles/src/copier.rs @@ -3,21 +3,23 @@ use std::path::PathBuf; #[cfg(feature = "cli")] use clap::{builder::ValueParser, error::ErrorKind, Args, ValueEnum}; +use enum_display::EnumDisplay; +use log::{debug, info}; use sqlite_hashes::rusqlite; use sqlite_hashes::rusqlite::params_from_iter; -use sqlx::sqlite::SqliteConnectOptions; -use sqlx::{query, Connection, Executor as _, Row, SqliteConnection}; +use sqlx::{query, Executor as _, Row, SqliteConnection}; use crate::errors::MbtResult; +use crate::mbtiles::MbtType; use crate::mbtiles::MbtType::{Flat, FlatWithHash, Normalized}; -use crate::mbtiles::{attach_hash_fn, MbtType}; use crate::queries::{ create_flat_tables, create_flat_with_hash_tables, create_normalized_tables, - create_tiles_with_hash_view, + create_tiles_with_hash_view, detach_db, is_empty_database, }; -use crate::{MbtError, Mbtiles}; +use crate::{MbtError, Mbtiles, AGG_TILES_HASH, AGG_TILES_HASH_IN_DIFF}; -#[derive(PartialEq, Eq, Default, Debug, Clone)] +#[derive(PartialEq, Eq, Default, Debug, Clone, EnumDisplay)] +#[enum_display(case = "Kebab")] #[cfg_attr(feature = "cli", derive(ValueEnum))] pub enum CopyDuplicateMode { #[default] @@ -30,30 +32,31 @@ pub enum CopyDuplicateMode { #[cfg_attr(feature = "cli", derive(Args))] pub struct MbtilesCopier { /// MBTiles file to read from - src_file: PathBuf, + pub src_file: PathBuf, /// MBTiles file to write to - dst_file: PathBuf, + pub dst_file: PathBuf, /// Output format of the destination file, ignored if the file exists. If not specified, defaults to the type of source #[cfg_attr(feature = "cli", arg(long, value_enum))] - dst_type: Option, + pub dst_type: Option, /// Specify copying behaviour when tiles with duplicate (zoom_level, tile_column, tile_row) values are found #[cfg_attr(feature = "cli", arg(long, value_enum, default_value_t = CopyDuplicateMode::default()))] - on_duplicate: CopyDuplicateMode, + pub on_duplicate: CopyDuplicateMode, /// Minimum zoom level to copy #[cfg_attr(feature = "cli", arg(long, conflicts_with("zoom_levels")))] - min_zoom: Option, + pub min_zoom: Option, /// Maximum zoom level to copy #[cfg_attr(feature = "cli", arg(long, conflicts_with("zoom_levels")))] - max_zoom: Option, + pub max_zoom: Option, /// List of zoom levels to copy #[cfg_attr(feature = "cli", arg(long, value_parser(ValueParser::new(HashSetValueParser{})), default_value=""))] - zoom_levels: HashSet, - /// Compare source file with this file, and only copy non-identical tiles to destination + pub zoom_levels: HashSet, + /// Compare source file with this file, and only copy non-identical tiles to destination. + /// It should be later possible to run `mbtiles apply-diff SRC_FILE DST_FILE` to get the same DIFF file. #[cfg_attr(feature = "cli", arg(long))] - diff_with_file: Option, + pub diff_with_file: Option, /// Skip generating a global hash for mbtiles validation. By default, `mbtiles` will compute `agg_tiles_hash` metadata value. #[cfg_attr(feature = "cli", arg(long))] - skip_agg_tiles_hash: bool, + pub skip_agg_tiles_hash: bool, } #[cfg(feature = "cli")] @@ -111,48 +114,6 @@ impl MbtilesCopier { } } - #[must_use] - pub fn dst_type(mut self, dst_type: Option) -> Self { - self.dst_type = dst_type; - self - } - - #[must_use] - pub fn on_duplicate(mut self, on_duplicate: CopyDuplicateMode) -> Self { - self.on_duplicate = on_duplicate; - self - } - - #[must_use] - pub fn zoom_levels(mut self, zoom_levels: Vec) -> Self { - self.zoom_levels.extend(zoom_levels); - self - } - - #[must_use] - pub fn min_zoom(mut self, min_zoom: Option) -> Self { - self.min_zoom = min_zoom; - self - } - - #[must_use] - pub fn max_zoom(mut self, max_zoom: Option) -> Self { - self.max_zoom = max_zoom; - self - } - - #[must_use] - pub fn diff_with_file(mut self, diff_with_file: PathBuf) -> Self { - self.diff_with_file = Some(diff_with_file); - self - } - - #[must_use] - pub fn skip_agg_tiles_hash(mut self, skip_global_hash: bool) -> Self { - self.skip_agg_tiles_hash = skip_global_hash; - self - } - pub async fn run(self) -> MbtResult { MbtileCopierInt::new(self)?.run().await } @@ -160,6 +121,15 @@ impl MbtilesCopier { impl MbtileCopierInt { pub fn new(options: MbtilesCopier) -> MbtResult { + // We may want to resolve the files to absolute paths here, but will need to avoid various non-file cases + if options.src_file == options.dst_file { + return Err(MbtError::SameSourceAndDestination(options.src_file)); + } + if let Some(diff_file) = &options.diff_with_file { + if options.src_file == *diff_file || options.dst_file == *diff_file { + return Err(MbtError::SameDiffAndSourceOrDestination(options.src_file)); + } + } Ok(MbtileCopierInt { src_mbtiles: Mbtiles::new(&options.src_file)?, dst_mbtiles: Mbtiles::new(&options.dst_file)?, @@ -168,122 +138,175 @@ impl MbtileCopierInt { } pub async fn run(self) -> MbtResult { - // src file connection is not needed after this point, as it will be attached to the dst file - let src_type = self.src_mbtiles.open_and_detect_type().await?; + // src and diff file connections are not needed later, as they will be attached to the dst file + let src_mbt = &self.src_mbtiles; + let dst_mbt = &self.dst_mbtiles; + + let src_type = src_mbt.open_and_detect_type().await?; + let dif = if let Some(dif_file) = &self.options.diff_with_file { + let dif_file = Mbtiles::new(dif_file)?; + let dif_type = dif_file.open_and_detect_type().await?; + Some((dif_file, dif_type)) + } else { + None + }; - let mut conn = SqliteConnection::connect_with( - &SqliteConnectOptions::new() - .create_if_missing(true) - .filename(&self.options.dst_file), - ) - .await?; + let mut conn = dst_mbt.open_or_new().await?; + let is_empty_db = is_empty_database(&mut conn).await?; + src_mbt.attach_to(&mut conn, "sourceDb").await?; + + let dst_type; + if let Some((dif_mbt, dif_type)) = &dif { + if !is_empty_db { + return Err(MbtError::NonEmptyTargetFile(self.options.dst_file)); + } + dst_type = self.options.dst_type.unwrap_or(src_type); + dif_mbt.attach_to(&mut conn, "diffDb").await?; + let dif_path = dif_mbt.filepath(); + info!("Comparing {src_mbt} ({src_type}) and {dif_path} ({dif_type}) into a new file {dst_mbt} ({dst_type})"); + } else if is_empty_db { + dst_type = self.options.dst_type.unwrap_or(src_type); + info!("Copying {src_mbt} ({src_type}) to a new file {dst_mbt} ({dst_type})"); + } else { + dst_type = dst_mbt.detect_type(&mut conn).await?; + info!("Copying {src_mbt} ({src_type}) to an existing file {dst_mbt} ({dst_type})"); + } - attach_hash_fn(&mut conn).await?; + if is_empty_db { + self.init_new_schema(&mut conn, src_type, dst_type).await?; + } - let is_empty = query!("SELECT 1 as has_rows FROM sqlite_schema LIMIT 1") - .fetch_optional(&mut conn) - .await? - .is_none(); - - let dst_type = if is_empty { - let dst_type = self.options.dst_type.unwrap_or(src_type); - self.copy_to_new(&mut conn, src_type, dst_type).await?; - dst_type - } else if self.options.diff_with_file.is_some() { - return Err(MbtError::NonEmptyTargetFile(self.options.dst_file)); + let select_from = if let Some((_, dif_type)) = &dif { + Self::get_select_from_with_diff(*dif_type, dst_type) } else { - let dst_type = self.dst_mbtiles.detect_type(&mut conn).await?; - self.src_mbtiles.attach_to(&mut conn, "sourceDb").await?; - dst_type + Self::get_select_from(src_type, dst_type).to_string() }; + let (where_clause, query_args) = self.get_where_clause(); + let select_from = format!("{select_from} {where_clause}"); let (on_dupl, sql_cond) = self.get_on_duplicate_sql(dst_type); - let (select_from, query_args) = { - let select_from = if let Some(diff_file) = &self.options.diff_with_file { - let diff_with_mbtiles = Mbtiles::new(diff_file)?; - let diff_type = diff_with_mbtiles.open_and_detect_type().await?; - diff_with_mbtiles.attach_to(&mut conn, "newDb").await?; - Self::get_select_from_with_diff(dst_type, diff_type) - } else { - Self::get_select_from(dst_type, src_type).to_string() - }; - - let (options_sql, query_args) = self.get_options_sql(); + debug!("Copying tiles with 'INSERT {on_dupl}' {src_type} -> {dst_type} ({sql_cond})"); + // Make sure not to execute any other queries while the handle is locked + let mut handle_lock = conn.lock_handle().await?; + let handle = handle_lock.as_raw_handle().as_ptr(); + + // SAFETY: this is safe as long as handle_lock is valid. We will drop the lock. + let rusqlite_conn = unsafe { rusqlite::Connection::from_handle(handle) }?; + + match dst_type { + Flat => { + let sql = format!( + " + INSERT {on_dupl} INTO tiles + (zoom_level, tile_column, tile_row, tile_data) + {select_from} {sql_cond}" + ); + debug!("Copying to {dst_type} with {sql} {query_args:?}"); + rusqlite_conn.execute(&sql, params_from_iter(query_args))? + } + FlatWithHash => { + let sql = format!( + " + INSERT {on_dupl} INTO tiles_with_hash + (zoom_level, tile_column, tile_row, tile_data, tile_hash) + {select_from} {sql_cond}" + ); + debug!("Copying to {dst_type} with {sql} {query_args:?}"); + rusqlite_conn.execute(&sql, params_from_iter(query_args))? + } + Normalized => { + let sql = format!( + " + INSERT OR IGNORE INTO images + (tile_id, tile_data) + SELECT hash as tile_id, tile_data + FROM ({select_from})" + ); + debug!("Copying to {dst_type} with {sql} {query_args:?}"); + rusqlite_conn.execute(&sql, params_from_iter(&query_args))?; + + let sql = format!( + " + INSERT {on_dupl} INTO map + (zoom_level, tile_column, tile_row, tile_id) + SELECT zoom_level, tile_column, tile_row, hash as tile_id + FROM ({select_from} {sql_cond})" + ); + debug!("Copying to {dst_type} with {sql} {query_args:?}"); + rusqlite_conn.execute(&sql, params_from_iter(query_args))? + } + }; - (format!("{select_from} {options_sql}"), query_args) + let sql = if self.options.diff_with_file.is_some() { + debug!("Copying metadata with 'INSERT {on_dupl}', taking into account diff file"); + // Insert all rows from diffDb.metadata if they do not exist or are different in sourceDb.metadata. + // Also insert all names from sourceDb.metadata that do not exist in diffDb.metadata, with their value set to NULL. + // Rename agg_tiles_hash to agg_tiles_hash_in_diff because agg_tiles_hash will be auto-added later + format!( + " + INSERT {on_dupl} INTO metadata (name, value) + SELECT IIF(dif.name = '{AGG_TILES_HASH}','{AGG_TILES_HASH_IN_DIFF}', dif.name) as name, + dif.value as value + FROM diffDb.metadata AS dif LEFT JOIN sourceDb.metadata AS src + ON dif.name = src.name + WHERE (dif.value != src.value OR src.value ISNULL) + AND dif.name != '{AGG_TILES_HASH_IN_DIFF}' + UNION ALL + SELECT src.name as name, NULL as value + FROM sourceDb.metadata AS src LEFT JOIN diffDb.metadata AS dif + ON src.name = dif.name + WHERE dif.value ISNULL AND src.name NOT IN ('{AGG_TILES_HASH}', '{AGG_TILES_HASH_IN_DIFF}');" + ) + } else { + debug!("Copying metadata with 'INSERT {on_dupl}'"); + format!("INSERT {on_dupl} INTO metadata SELECT name, value FROM sourceDb.metadata") }; + rusqlite_conn.execute(&sql, [])?; - { - // Make sure not to execute any other queries while the handle is locked - let mut handle_lock = conn.lock_handle().await?; - let handle = handle_lock.as_raw_handle().as_ptr(); - - // SAFETY: this is safe as long as handle_lock is valid - let rusqlite_conn = unsafe { rusqlite::Connection::from_handle(handle) }?; - match dst_type { - Flat => rusqlite_conn.execute( - &format!("INSERT {on_dupl} INTO tiles {select_from} {sql_cond}"), - params_from_iter(query_args), - )?, - FlatWithHash => rusqlite_conn.execute( - &format!("INSERT {on_dupl} INTO tiles_with_hash {select_from} {sql_cond}"), - params_from_iter(query_args), - )?, - Normalized => { - rusqlite_conn.execute( - &format!( - "INSERT {on_dupl} INTO map (zoom_level, tile_column, tile_row, tile_id) - SELECT zoom_level, tile_column, tile_row, hash as tile_id - FROM ({select_from} {sql_cond})" - ), - params_from_iter(&query_args), - )?; - rusqlite_conn.execute( - &format!( - "INSERT OR IGNORE INTO images SELECT tile_data, hash FROM ({select_from})" - ), - params_from_iter(query_args), - )? - } - }; - } + // SAFETY: must drop rusqlite_conn before handle_lock, or place the code since lock in a separate scope + drop(rusqlite_conn); + drop(handle_lock); if !self.options.skip_agg_tiles_hash { - self.dst_mbtiles.update_agg_tiles_hash(&mut conn).await?; + dst_mbt.update_agg_tiles_hash(&mut conn).await?; } + detach_db(&mut conn, "sourceDb").await?; + // Ignore error because we might not have attached diffDb + let _ = detach_db(&mut conn, "diffDb").await; + Ok(conn) } - async fn copy_to_new( + async fn init_new_schema( &self, conn: &mut SqliteConnection, src: MbtType, dst: MbtType, ) -> MbtResult<()> { + debug!("Resetting PRAGMA settings and vacuuming"); query!("PRAGMA page_size = 512").execute(&mut *conn).await?; query!("PRAGMA encoding = 'UTF-8'") .execute(&mut *conn) .await?; query!("VACUUM").execute(&mut *conn).await?; - self.src_mbtiles.attach_to(&mut *conn, "sourceDb").await?; - if src == dst { // DB objects must be created in a specific order: tables, views, triggers, indexes. + debug!("Copying DB schema verbatim"); let sql_objects = conn .fetch_all( "SELECT sql FROM sourceDb.sqlite_schema WHERE tbl_name IN ('metadata', 'tiles', 'map', 'images', 'tiles_with_hash') - AND type IN ('table', 'view', 'trigger', 'index') + AND type IN ('table', 'view', 'trigger', 'index') ORDER BY CASE WHEN type = 'table' THEN 1 WHEN type = 'view' THEN 2 WHEN type = 'trigger' THEN 3 WHEN type = 'index' THEN 4 - ELSE 5 END", + ELSE 5 END;", ) .await?; @@ -303,9 +326,6 @@ impl MbtileCopierInt { create_tiles_with_hash_view(&mut *conn).await?; } - conn.execute("INSERT INTO metadata SELECT * FROM sourceDb.metadata") - .await?; - Ok(()) } @@ -336,46 +356,75 @@ impl MbtileCopierInt { } } - fn get_select_from_with_diff(dst_type: MbtType, diff_type: MbtType) -> String { - let (hash_col_sql, new_tiles_with_hash) = if dst_type == Flat { - ("", "newDb.tiles") + fn get_select_from_with_diff(dif_type: MbtType, dst_type: MbtType) -> String { + let hash_col_sql; + let diff_tiles; + if dst_type == Flat { + hash_col_sql = ""; + diff_tiles = "diffDb.tiles"; } else { - match diff_type { - Flat => (", hex(md5(tile_data)) as hash", "newDb.tiles"), - FlatWithHash => (", new_tiles_with_hash.tile_hash as hash", "newDb.tiles_with_hash"), - Normalized => (", new_tiles_with_hash.hash", - "(SELECT zoom_level, tile_column, tile_row, tile_data, map.tile_id AS hash - FROM newDb.map JOIN newDb.images ON newDb.map.tile_id = newDb.images.tile_id)"), - } - }; + hash_col_sql = match dif_type { + Flat => ", COALESCE(md5_hex(difTiles.tile_data), '') as hash", + FlatWithHash => ", COALESCE(difTiles.tile_hash, '') as hash", + Normalized => ", COALESCE(difTiles.hash, '') as hash", + }; + diff_tiles = match dif_type { + Flat => "diffDb.tiles", + FlatWithHash => "diffDb.tiles_with_hash", + Normalized => { + " + (SELECT zoom_level, tile_column, tile_row, tile_data, map.tile_id AS hash + FROM diffDb.map JOIN diffDb.images ON diffDb.map.tile_id = diffDb.images.tile_id)" + } + }; + } - format!("SELECT COALESCE(sourceDb.tiles.zoom_level, new_tiles_with_hash.zoom_level) as zoom_level, - COALESCE(sourceDb.tiles.tile_column, new_tiles_with_hash.tile_column) as tile_column, - COALESCE(sourceDb.tiles.tile_row, new_tiles_with_hash.tile_row) as tile_row, - new_tiles_with_hash.tile_data as tile_data - {hash_col_sql} - FROM sourceDb.tiles FULL JOIN {new_tiles_with_hash} AS new_tiles_with_hash - ON sourceDb.tiles.zoom_level = new_tiles_with_hash.zoom_level - AND sourceDb.tiles.tile_column = new_tiles_with_hash.tile_column - AND sourceDb.tiles.tile_row = new_tiles_with_hash.tile_row - WHERE (sourceDb.tiles.tile_data != new_tiles_with_hash.tile_data - OR sourceDb.tiles.tile_data ISNULL - OR new_tiles_with_hash.tile_data ISNULL)") + format!( + " + SELECT COALESCE(srcTiles.zoom_level, difTiles.zoom_level) as zoom_level + , COALESCE(srcTiles.tile_column, difTiles.tile_column) as tile_column + , COALESCE(srcTiles.tile_row, difTiles.tile_row) as tile_row + , difTiles.tile_data as tile_data + {hash_col_sql} + FROM sourceDb.tiles AS srcTiles FULL JOIN {diff_tiles} AS difTiles + ON srcTiles.zoom_level = difTiles.zoom_level + AND srcTiles.tile_column = difTiles.tile_column + AND srcTiles.tile_row = difTiles.tile_row + WHERE (srcTiles.tile_data != difTiles.tile_data + OR srcTiles.tile_data ISNULL + OR difTiles.tile_data ISNULL)" + ) } - fn get_select_from(dst_type: MbtType, src_type: MbtType) -> &'static str { + fn get_select_from(src_type: MbtType, dst_type: MbtType) -> &'static str { if dst_type == Flat { - "SELECT * FROM sourceDb.tiles WHERE TRUE" + "SELECT zoom_level, tile_column, tile_row, tile_data FROM sourceDb.tiles WHERE TRUE" } else { match src_type { - Flat => "SELECT zoom_level, tile_column, tile_row, tile_data, hex(md5(tile_data)) as hash FROM sourceDb.tiles WHERE TRUE", - FlatWithHash => "SELECT zoom_level, tile_column, tile_row, tile_data, tile_hash AS hash FROM sourceDb.tiles_with_hash WHERE TRUE", - Normalized => "SELECT zoom_level, tile_column, tile_row, tile_data, map.tile_id AS hash FROM sourceDb.map JOIN sourceDb.images ON sourceDb.map.tile_id = sourceDb.images.tile_id WHERE TRUE" + Flat => { + " + SELECT zoom_level, tile_column, tile_row, tile_data, md5_hex(tile_data) as hash + FROM sourceDb.tiles + WHERE TRUE" + } + FlatWithHash => { + " + SELECT zoom_level, tile_column, tile_row, tile_data, tile_hash AS hash + FROM sourceDb.tiles_with_hash + WHERE TRUE" + } + Normalized => { + " + SELECT zoom_level, tile_column, tile_row, tile_data, map.tile_id AS hash + FROM sourceDb.map JOIN sourceDb.images + ON sourceDb.map.tile_id = sourceDb.images.tile_id + WHERE TRUE" + } } } } - fn get_options_sql(&self) -> (String, Vec) { + fn get_where_clause(&self) -> (String, Vec) { let mut query_args = vec![]; let sql = if !&self.options.zoom_levels.is_empty() { @@ -406,56 +455,6 @@ impl MbtileCopierInt { } } -pub async fn apply_diff(src_file: PathBuf, diff_file: PathBuf) -> MbtResult<()> { - let src_mbtiles = Mbtiles::new(src_file)?; - let diff_mbtiles = Mbtiles::new(diff_file)?; - let diff_type = diff_mbtiles.open_and_detect_type().await?; - - let mut conn = src_mbtiles.open().await?; - diff_mbtiles.attach_to(&mut conn, "diffDb").await?; - - let src_type = src_mbtiles.detect_type(&mut conn).await?; - let select_from = if src_type == Flat { - "SELECT zoom_level, tile_column, tile_row, tile_data FROM diffDb.tiles" - } else { - match diff_type { - Flat => "SELECT zoom_level, tile_column, tile_row, tile_data, hex(md5(tile_data)) as hash FROM diffDb.tiles", - FlatWithHash => "SELECT zoom_level, tile_column, tile_row, tile_data, tile_hash AS hash FROM diffDb.tiles_with_hash", - Normalized => "SELECT zoom_level, tile_column, tile_row, tile_data, map.tile_id AS hash FROM diffDb.map LEFT JOIN diffDb.images ON diffDb.map.tile_id = diffDb.images.tile_id", - } - }.to_string(); - - let (main_table, insert_sql) = match src_type { - Flat => ("tiles", vec![ - format!("INSERT OR REPLACE INTO tiles (zoom_level, tile_column, tile_row, tile_data) {select_from}")]), - FlatWithHash => ("tiles_with_hash", vec![ - format!("INSERT OR REPLACE INTO tiles_with_hash {select_from}")]), - Normalized => ("map", vec![ - format!("INSERT OR REPLACE INTO map (zoom_level, tile_column, tile_row, tile_id) - SELECT zoom_level, tile_column, tile_row, hash as tile_id - FROM ({select_from})"), - format!("INSERT OR REPLACE INTO images SELECT tile_data, hash FROM ({select_from})"), - ]) - }; - - for statement in insert_sql { - query(&format!("{statement} WHERE tile_data NOTNULL")) - .execute(&mut conn) - .await?; - } - - query(&format!( - "DELETE FROM {main_table} - WHERE (zoom_level, tile_column, tile_row) IN ( - SELECT zoom_level, tile_column, tile_row FROM ({select_from} WHERE tile_data ISNULL) - )" - )) - .execute(&mut conn) - .await?; - - Ok(()) -} - #[cfg(test)] mod tests { use sqlx::{Decode, Sqlite, SqliteConnection, Type}; @@ -475,13 +474,12 @@ mod tests { dst_type: Option, expected_dst_type: MbtType, ) -> MbtResult<()> { - let mut dst_conn = MbtilesCopier::new(src_filepath.clone(), dst_filepath.clone()) - .dst_type(dst_type) - .run() - .await?; + let mut opt = MbtilesCopier::new(src_filepath.clone(), dst_filepath.clone()); + opt.dst_type = dst_type; + let mut dst_conn = opt.run().await?; Mbtiles::new(src_filepath)? - .attach_to(&mut dst_conn, "srcDb") + .attach_to(&mut dst_conn, "testSrcDb") .await?; assert_eq!( @@ -492,7 +490,7 @@ mod tests { ); assert!(dst_conn - .fetch_optional("SELECT * FROM srcDb.tiles EXCEPT SELECT * FROM tiles") + .fetch_optional("SELECT * FROM testSrcDb.tiles EXCEPT SELECT * FROM tiles") .await? .is_none()); @@ -500,10 +498,10 @@ mod tests { } async fn verify_copy_with_zoom_filter( - opts: MbtilesCopier, + opt: MbtilesCopier, expected_zoom_levels: u8, ) -> MbtResult<()> { - let mut dst_conn = opts.run().await?; + let mut dst_conn = opt.run().await?; assert_eq!( get_one::( @@ -594,9 +592,9 @@ mod tests { async fn copy_with_min_max_zoom() -> MbtResult<()> { let src = PathBuf::from("../tests/fixtures/mbtiles/world_cities.mbtiles"); let dst = PathBuf::from("file:copy_with_min_max_zoom_mem_db?mode=memory&cache=shared"); - let opt = MbtilesCopier::new(src, dst) - .min_zoom(Some(2)) - .max_zoom(Some(4)); + let mut opt = MbtilesCopier::new(src, dst); + opt.min_zoom = Some(2); + opt.max_zoom = Some(4); verify_copy_with_zoom_filter(opt, 3).await } @@ -604,10 +602,10 @@ mod tests { async fn copy_with_zoom_levels() -> MbtResult<()> { let src = PathBuf::from("../tests/fixtures/mbtiles/world_cities.mbtiles"); let dst = PathBuf::from("file:copy_with_zoom_levels_mem_db?mode=memory&cache=shared"); - let opt = MbtilesCopier::new(src, dst) - .min_zoom(Some(2)) - .max_zoom(Some(4)) - .zoom_levels(vec![1, 6]); + let mut opt = MbtilesCopier::new(src, dst); + opt.min_zoom = Some(2); + opt.max_zoom = Some(4); + opt.zoom_levels.extend(&[1, 6]); verify_copy_with_zoom_filter(opt, 2).await } @@ -619,10 +617,9 @@ mod tests { let diff_file = PathBuf::from("../tests/fixtures/mbtiles/geography-class-jpg-modified.mbtiles"); - let copy_opts = - MbtilesCopier::new(src.clone(), dst.clone()).diff_with_file(diff_file.clone()); - - let mut dst_conn = copy_opts.run().await?; + let mut opt = MbtilesCopier::new(src.clone(), dst.clone()); + opt.diff_with_file = Some(diff_file.clone()); + let mut dst_conn = opt.run().await?; assert!(dst_conn .fetch_optional("SELECT 1 FROM sqlite_schema WHERE name = 'tiles';") @@ -650,10 +647,10 @@ mod tests { assert!(get_one::>( &mut dst_conn, - "SELECT tile_id FROM map WHERE zoom_level = 0 AND tile_row = 0 AND tile_column = 0;" + "SELECT * FROM map WHERE zoom_level = 0 AND tile_row = 0 AND tile_column = 0;", ) .await - .is_none()); + .is_some()); Ok(()) } @@ -680,11 +677,11 @@ mod tests { let src = PathBuf::from("../tests/fixtures/mbtiles/world_cities_modified.mbtiles"); let dst = PathBuf::from("../tests/fixtures/mbtiles/world_cities.mbtiles"); - let copy_opts = - MbtilesCopier::new(src.clone(), dst.clone()).on_duplicate(CopyDuplicateMode::Abort); + let mut opt = MbtilesCopier::new(src.clone(), dst.clone()); + opt.on_duplicate = CopyDuplicateMode::Abort; assert!(matches!( - copy_opts.run().await.unwrap_err(), + opt.run().await.unwrap_err(), MbtError::RusqliteError(..) )); } @@ -708,10 +705,10 @@ mod tests { // Verify the tiles in the destination file is a superset of the tiles in the source file Mbtiles::new(src_file)? - .attach_to(&mut dst_conn, "otherDb") + .attach_to(&mut dst_conn, "testOtherDb") .await?; assert!(dst_conn - .fetch_optional("SELECT * FROM otherDb.tiles EXCEPT SELECT * FROM tiles;") + .fetch_optional("SELECT * FROM testOtherDb.tiles EXCEPT SELECT * FROM tiles;") .await? .is_none()); @@ -731,17 +728,16 @@ mod tests { .run() .await?; - let mut dst_conn = MbtilesCopier::new(src_file.clone(), dst.clone()) - .on_duplicate(CopyDuplicateMode::Ignore) - .run() - .await?; + let mut opt = MbtilesCopier::new(src_file.clone(), dst.clone()); + opt.on_duplicate = CopyDuplicateMode::Ignore; + let mut dst_conn = opt.run().await?; // Verify the tiles in the destination file are the same as those in the source file except for those with duplicate (zoom_level, tile_column, tile_row) Mbtiles::new(src_file)? - .attach_to(&mut dst_conn, "srcDb") + .attach_to(&mut dst_conn, "testSrcDb") .await?; Mbtiles::new(dst_file)? - .attach_to(&mut dst_conn, "originalDb") + .attach_to(&mut dst_conn, "testOriginalDb") .await?; // Create a temporary table with all the tiles in the original database and @@ -752,8 +748,8 @@ mod tests { COALESCE(t1.tile_column, t2.zoom_level) as tile_column, COALESCE(t1.tile_row, t2.tile_row) as tile_row, COALESCE(t1.tile_data, t2.tile_data) as tile_data - FROM originalDb.tiles as t1 - FULL OUTER JOIN srcDb.tiles as t2 + FROM testOriginalDb.tiles as t1 + FULL OUTER JOIN testSrcDb.tiles as t2 ON t1.zoom_level = t2.zoom_level AND t1.tile_column = t2.tile_column AND t1.tile_row = t2.tile_row") .await?; @@ -769,58 +765,4 @@ mod tests { Ok(()) } - - #[actix_rt::test] - async fn apply_flat_diff_file() -> MbtResult<()> { - // Copy the src file to an in-memory DB - let src_file = PathBuf::from("../tests/fixtures/mbtiles/world_cities.mbtiles"); - let src = PathBuf::from("file:apply_flat_diff_file_mem_db?mode=memory&cache=shared"); - - let mut src_conn = MbtilesCopier::new(src_file.clone(), src.clone()) - .run() - .await?; - - // Apply diff to the src data in in-memory DB - let diff_file = PathBuf::from("../tests/fixtures/mbtiles/world_cities_diff.mbtiles"); - apply_diff(src, diff_file).await?; - - // Verify the data is the same as the file the diff was generated from - Mbtiles::new("../tests/fixtures/mbtiles/world_cities_modified.mbtiles")? - .attach_to(&mut src_conn, "otherDb") - .await?; - - assert!(src_conn - .fetch_optional("SELECT * FROM tiles EXCEPT SELECT * FROM otherDb.tiles;") - .await? - .is_none()); - - Ok(()) - } - - #[actix_rt::test] - async fn apply_normalized_diff_file() -> MbtResult<()> { - // Copy the src file to an in-memory DB - let src_file = PathBuf::from("../tests/fixtures/mbtiles/geography-class-jpg.mbtiles"); - let src = PathBuf::from("file:apply_normalized_diff_file_mem_db?mode=memory&cache=shared"); - - let mut src_conn = MbtilesCopier::new(src_file.clone(), src.clone()) - .run() - .await?; - - // Apply diff to the src data in in-memory DB - let diff_file = PathBuf::from("../tests/fixtures/mbtiles/geography-class-jpg-diff.mbtiles"); - apply_diff(src, diff_file).await?; - - // Verify the data is the same as the file the diff was generated from - Mbtiles::new("../tests/fixtures/mbtiles/geography-class-jpg-modified.mbtiles")? - .attach_to(&mut src_conn, "otherDb") - .await?; - - assert!(src_conn - .fetch_optional("SELECT * FROM tiles EXCEPT SELECT * FROM otherDb.tiles;") - .await? - .is_none()); - - Ok(()) - } } diff --git a/martin-mbtiles/src/errors.rs b/martin-mbtiles/src/errors.rs index 43d200037..f8b8b14ab 100644 --- a/martin-mbtiles/src/errors.rs +++ b/martin-mbtiles/src/errors.rs @@ -5,6 +5,12 @@ use sqlite_hashes::rusqlite; #[derive(thiserror::Error, Debug)] pub enum MbtError { + #[error("The source and destination MBTiles files are the same: {}", .0.display())] + SameSourceAndDestination(PathBuf), + + #[error("The diff file and source or destination MBTiles files are the same: {}", .0.display())] + SameDiffAndSourceOrDestination(PathBuf), + #[error("SQL Error {0}")] SqlxError(#[from] sqlx::Error), @@ -38,7 +44,7 @@ pub enum MbtError { #[error("No tiles found")] NoTilesFound, - #[error("The destination file {0} is non-empty")] + #[error("The destination file {0} is not empty. Some operations like creating a diff file require the destination file to be non-existent or empty.")] NonEmptyTargetFile(PathBuf), #[error("The file {0} does not have the required uniqueness constraint")] diff --git a/martin-mbtiles/src/lib.rs b/martin-mbtiles/src/lib.rs index dff5b1052..d31dcb7af 100644 --- a/martin-mbtiles/src/lib.rs +++ b/martin-mbtiles/src/lib.rs @@ -4,12 +4,22 @@ mod errors; pub use errors::{MbtError, MbtResult}; mod mbtiles; -pub use mbtiles::{IntegrityCheckType, Mbtiles, Metadata}; +pub use mbtiles::{ + calc_agg_tiles_hash, IntegrityCheckType, MbtType, Mbtiles, Metadata, AGG_TILES_HASH, + AGG_TILES_HASH_IN_DIFF, +}; mod pool; pub use pool::MbtilesPool; mod copier; -pub use copier::{apply_diff, CopyDuplicateMode, MbtilesCopier}; +pub use copier::{CopyDuplicateMode, MbtilesCopier}; + +mod patcher; +pub use patcher::apply_patch; mod queries; +pub use queries::{ + create_flat_tables, create_flat_with_hash_tables, create_metadata_table, + create_normalized_tables, is_flat_with_hash_tables_type, is_normalized_tables_type, +}; diff --git a/martin-mbtiles/src/mbtiles.rs b/martin-mbtiles/src/mbtiles.rs index 6cedee852..faf37a043 100644 --- a/martin-mbtiles/src/mbtiles.rs +++ b/martin-mbtiles/src/mbtiles.rs @@ -8,6 +8,7 @@ use std::str::FromStr; #[cfg(feature = "cli")] use clap::ValueEnum; +use enum_display::EnumDisplay; use futures::TryStreamExt; use log::{debug, info, warn}; use martin_tile_utils::{Format, TileInfo}; @@ -54,7 +55,15 @@ where s.end() } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +/// Metadata key for the aggregate tiles hash value +pub const AGG_TILES_HASH: &str = "agg_tiles_hash"; + +/// Metadata key for a diff file, +/// describing the eventual AGG_TILES_HASH value once the diff is applied +pub const AGG_TILES_HASH_IN_DIFF: &str = "agg_tiles_hash_after_apply"; + +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, EnumDisplay)] +#[enum_display(case = "Kebab")] #[cfg_attr(feature = "cli", derive(ValueEnum))] pub enum MbtType { Flat, @@ -62,7 +71,8 @@ pub enum MbtType { Normalized, } -#[derive(PartialEq, Eq, Default, Debug, Clone)] +#[derive(PartialEq, Eq, Default, Debug, Clone, EnumDisplay)] +#[enum_display(case = "Kebab")] #[cfg_attr(feature = "cli", derive(ValueEnum))] pub enum IntegrityCheckType { #[default] @@ -77,6 +87,12 @@ pub struct Mbtiles { filename: String, } +impl Display for Mbtiles { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.filepath) + } +} + impl Mbtiles { pub fn new>(filepath: P) -> MbtResult { let path = filepath.as_ref(); @@ -94,11 +110,13 @@ impl Mbtiles { } pub async fn open(&self) -> MbtResult { + debug!("Opening w/ defaults {self}"); let opt = SqliteConnectOptions::new().filename(self.filepath()); Self::open_int(&opt).await } pub async fn open_or_new(&self) -> MbtResult { + debug!("Opening or creating {self}"); let opt = SqliteConnectOptions::new() .filename(self.filepath()) .create_if_missing(true); @@ -106,6 +124,7 @@ impl Mbtiles { } pub async fn open_readonly(&self) -> MbtResult { + debug!("Opening as readonly {self}"); let opt = SqliteConnectOptions::new() .filename(self.filepath()) .read_only(true); @@ -144,6 +163,7 @@ impl Mbtiles { where for<'e> &'e mut T: SqliteExecutor<'e>, { + debug!("Attaching {self} as {name}"); query(&format!("ATTACH DATABASE ? AS {name}")) .bind(self.filepath()) .execute(conn) @@ -166,19 +186,38 @@ impl Mbtiles { Ok(None) } + pub async fn validate( + &self, + check_type: IntegrityCheckType, + update_agg_tiles_hash: bool, + ) -> MbtResult { + let mut conn = if update_agg_tiles_hash { + self.open().await? + } else { + self.open_readonly().await? + }; + self.check_integrity(&mut conn, check_type).await?; + self.check_each_tile_hash(&mut conn).await?; + if update_agg_tiles_hash { + self.update_agg_tiles_hash(&mut conn).await + } else { + self.check_agg_tiles_hashes(&mut conn).await + } + } + /// Get the aggregate tiles hash value from the metadata table pub async fn get_agg_tiles_hash(&self, conn: &mut T) -> MbtResult> where for<'e> &'e mut T: SqliteExecutor<'e>, { - self.get_metadata_value(&mut *conn, "agg_tiles_hash").await + self.get_metadata_value(&mut *conn, AGG_TILES_HASH).await } pub async fn set_metadata_value( &self, conn: &mut T, key: &str, - value: Option, + value: Option<&str>, ) -> MbtResult<()> where for<'e> &'e mut T: SqliteExecutor<'e>, @@ -397,6 +436,7 @@ impl Mbtiles { where for<'e> &'e mut T: SqliteExecutor<'e>, { + debug!("Detecting MBTiles type for {self}"); let mbt_type = if is_normalized_tables_type(&mut *conn).await? { MbtType::Normalized } else if is_flat_with_hash_tables_type(&mut *conn).await? { @@ -469,9 +509,8 @@ impl Mbtiles { where for<'e> &'e mut T: SqliteExecutor<'e>, { - let filepath = self.filepath(); if integrity_check == IntegrityCheckType::Off { - info!("Skipping integrity check for {filepath}"); + info!("Skipping integrity check for {self}"); return Ok(()); } @@ -488,55 +527,53 @@ impl Mbtiles { if result.len() > 1 || result.get(0).ok_or(FailedIntegrityCheck( - filepath.to_string(), + self.filepath.to_string(), vec!["SQLite could not perform integrity check".to_string()], ))? != "ok" { return Err(FailedIntegrityCheck(self.filepath().to_string(), result)); } - info!("{integrity_check:?} integrity check passed for {filepath}"); + info!("{integrity_check:?} integrity check passed for {self}"); Ok(()) } - pub async fn check_agg_tiles_hashes(&self, conn: &mut T) -> MbtResult<()> + pub async fn check_agg_tiles_hashes(&self, conn: &mut T) -> MbtResult where for<'e> &'e mut T: SqliteExecutor<'e>, { - let filepath = self.filepath(); let Some(stored) = self.get_agg_tiles_hash(&mut *conn).await? else { - return Err(AggHashValueNotFound(filepath.to_string())); + return Err(AggHashValueNotFound(self.filepath().to_string())); }; let computed = calc_agg_tiles_hash(&mut *conn).await?; if stored != computed { - let file = filepath.to_string(); + let file = self.filepath().to_string(); return Err(AggHashMismatch(computed, stored, file)); } - info!("The agg_tiles_hashes={computed} has been verified for {filepath}"); - Ok(()) + info!("The agg_tiles_hashes={computed} has been verified for {self}"); + Ok(computed) } /// Compute new aggregate tiles hash and save it to the metadata table (if needed) - pub async fn update_agg_tiles_hash(&self, conn: &mut T) -> MbtResult<()> + pub async fn update_agg_tiles_hash(&self, conn: &mut T) -> MbtResult where for<'e> &'e mut T: SqliteExecutor<'e>, { let old_hash = self.get_agg_tiles_hash(&mut *conn).await?; let hash = calc_agg_tiles_hash(&mut *conn).await?; - let path = self.filepath(); if old_hash.as_ref() == Some(&hash) { - info!("agg_tiles_hash is already set to the correct value `{hash}` in {path}"); - Ok(()) + info!("Metadata value agg_tiles_hash is already set to the correct hash `{hash}` in {self}"); } else { if let Some(old_hash) = old_hash { - info!("Updating agg_tiles_hash from {old_hash} to {hash} in {path}"); + info!("Updating agg_tiles_hash from {old_hash} to {hash} in {self}"); } else { - info!("Creating new metadata value agg_tiles_hash = {hash} in {path}"); + info!("Adding a new metadata value agg_tiles_hash = {hash} in {self}"); } - self.set_metadata_value(&mut *conn, "agg_tiles_hash", Some(hash)) - .await + self.set_metadata_value(&mut *conn, AGG_TILES_HASH, Some(&hash)) + .await?; } + Ok(hash) } pub async fn check_each_tile_hash(&self, conn: &mut T) -> MbtResult<()> @@ -546,14 +583,14 @@ impl Mbtiles { // Note that hex() always returns upper-case HEX values let sql = match self.detect_type(&mut *conn).await? { MbtType::Flat => { - println!("Skipping per-tile hash validation because this is a flat MBTiles file"); + info!("Skipping per-tile hash validation because this is a flat MBTiles file"); return Ok(()); } MbtType::FlatWithHash => { "SELECT expected, computed FROM ( SELECT upper(tile_hash) AS expected, - hex(md5(tile_data)) AS computed + md5_hex(tile_data) AS computed FROM tiles_with_hash ) AS t WHERE expected != computed @@ -563,7 +600,7 @@ impl Mbtiles { "SELECT expected, computed FROM ( SELECT upper(tile_id) AS expected, - hex(md5(tile_data)) AS computed + md5_hex(tile_data) AS computed FROM images ) AS t WHERE expected != computed @@ -582,36 +619,42 @@ impl Mbtiles { )) })?; - info!("All tile hashes are valid for {}", self.filepath()); + info!("All tile hashes are valid for {self}"); Ok(()) } } /// Compute the hash of the combined tiles in the mbtiles file tiles table/view. /// This should work on all mbtiles files perf `MBTiles` specification. -async fn calc_agg_tiles_hash(conn: &mut T) -> MbtResult +pub async fn calc_agg_tiles_hash(conn: &mut T) -> MbtResult where for<'e> &'e mut T: SqliteExecutor<'e>, { + debug!("Calculating agg_tiles_hash"); let query = query( // The md5_concat func will return NULL if there are no rows in the tiles table. // For our use case, we will treat it as an empty string, and hash that. // `tile_data` values must be stored as a blob per MBTiles spec // `md5` functions will fail if the value is not text/blob/null - "SELECT - hex( - coalesce( - md5_concat( + // + // Note that ORDER BY controls the output ordering, which is important for the hash value, + // and having it at the top level would not order values properly. + // See https://sqlite.org/forum/forumpost/228bb96e12a746ce + " +SELECT coalesce( + (SELECT md5_concat_hex( cast(zoom_level AS text), cast(tile_column AS text), cast(tile_row AS text), tile_data - ), - md5('') ) - ) - FROM tiles - ORDER BY zoom_level, tile_column, tile_row;", + OVER (ORDER BY zoom_level, tile_column, tile_row ROWS + BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) + FROM tiles + LIMIT 1), + md5_hex('') +); +", ); Ok(query.fetch_one(conn).await?.get::(0)) } @@ -638,9 +681,7 @@ mod tests { async fn open(filepath: &str) -> MbtResult<(SqliteConnection, Mbtiles)> { let mbt = Mbtiles::new(filepath)?; - let mut conn = SqliteConnection::connect(mbt.filepath()).await?; - attach_hash_fn(&mut conn).await?; - Ok((conn, mbt)) + mbt.open().await.map(|conn| (conn, mbt)) } #[actix_rt::test] @@ -726,7 +767,7 @@ mod tests { conn.execute("CREATE TABLE metadata (name text NOT NULL PRIMARY KEY, value text);") .await?; - mbt.set_metadata_value(&mut conn, "bounds", Some("0.0, 0.0, 0.0, 0.0".to_string())) + mbt.set_metadata_value(&mut conn, "bounds", Some("0.0, 0.0, 0.0, 0.0")) .await?; assert_eq!( mbt.get_metadata_value(&mut conn, "bounds").await?.unwrap(), @@ -736,7 +777,7 @@ mod tests { mbt.set_metadata_value( &mut conn, "bounds", - Some("-123.123590,-37.818085,174.763027,59.352706".to_string()), + Some("-123.123590,-37.818085,174.763027,59.352706"), ) .await?; assert_eq!( diff --git a/martin-mbtiles/src/patcher.rs b/martin-mbtiles/src/patcher.rs new file mode 100644 index 000000000..d56cb5cfd --- /dev/null +++ b/martin-mbtiles/src/patcher.rs @@ -0,0 +1,189 @@ +use std::path::PathBuf; + +use log::{debug, info}; +use sqlx::query; + +use crate::queries::detach_db; +use crate::MbtType::{Flat, FlatWithHash, Normalized}; +use crate::{MbtResult, Mbtiles, AGG_TILES_HASH, AGG_TILES_HASH_IN_DIFF}; + +pub async fn apply_patch(src_file: PathBuf, patch_file: PathBuf) -> MbtResult<()> { + let src_mbt = Mbtiles::new(src_file)?; + let patch_mbt = Mbtiles::new(patch_file)?; + let patch_type = patch_mbt.open_and_detect_type().await?; + + let mut conn = src_mbt.open().await?; + let src_type = src_mbt.detect_type(&mut conn).await?; + patch_mbt.attach_to(&mut conn, "patchDb").await?; + + info!("Applying patch file {patch_mbt} ({patch_type}) to {src_mbt} ({src_type})"); + let select_from = if src_type == Flat { + "SELECT zoom_level, tile_column, tile_row, tile_data FROM patchDb.tiles" + } else { + match patch_type { + Flat => { + " + SELECT zoom_level, tile_column, tile_row, tile_data, md5_hex(tile_data) as hash + FROM patchDb.tiles" + } + FlatWithHash => { + " + SELECT zoom_level, tile_column, tile_row, tile_data, tile_hash AS hash + FROM patchDb.tiles_with_hash" + } + Normalized => { + " + SELECT zoom_level, tile_column, tile_row, tile_data, map.tile_id AS hash + FROM patchDb.map LEFT JOIN patchDb.images + ON patchDb.map.tile_id = patchDb.images.tile_id" + } + } + } + .to_string(); + + let (main_table, insert_sql) = match src_type { + Flat => ( + "tiles", + vec![format!( + " + INSERT OR REPLACE INTO tiles (zoom_level, tile_column, tile_row, tile_data) + {select_from}" + )], + ), + FlatWithHash => ( + "tiles_with_hash", + vec![format!( + " + INSERT OR REPLACE INTO tiles_with_hash (zoom_level, tile_column, tile_row, tile_data, tile_hash) + {select_from}" + )], + ), + Normalized => ( + "map", + vec![ + format!( + " + INSERT OR REPLACE INTO map (zoom_level, tile_column, tile_row, tile_id) + SELECT zoom_level, tile_column, tile_row, hash as tile_id + FROM ({select_from})" + ), + format!( + " + INSERT OR REPLACE INTO images (tile_id, tile_data) + SELECT hash as tile_id, tile_data + FROM ({select_from})" + ), + ], + ), + }; + + for statement in insert_sql { + query(&format!("{statement} WHERE tile_data NOTNULL")) + .execute(&mut conn) + .await?; + } + + query(&format!( + " + DELETE FROM {main_table} + WHERE (zoom_level, tile_column, tile_row) IN ( + SELECT zoom_level, tile_column, tile_row FROM ({select_from} WHERE tile_data ISNULL) + )" + )) + .execute(&mut conn) + .await?; + + if src_type == Normalized { + debug!("Removing unused tiles from the images table (normalized schema)"); + query("DELETE FROM images WHERE tile_id NOT IN (SELECT tile_id FROM map)") + .execute(&mut conn) + .await?; + } + + // Copy metadata from patchDb to the destination file, replacing existing values + // Convert 'agg_tiles_hash_in_patch' into 'agg_tiles_hash' + // Delete metadata entries if the value is NULL in patchDb + query(&format!( + " + INSERT OR REPLACE INTO metadata (name, value) + SELECT IIF(name = '{AGG_TILES_HASH_IN_DIFF}', '{AGG_TILES_HASH}', name) as name, + value + FROM patchDb.metadata + WHERE name NOTNULL AND name != '{AGG_TILES_HASH}';" + )) + .execute(&mut conn) + .await?; + + query( + " + DELETE FROM metadata + WHERE name IN (SELECT name FROM patchDb.metadata WHERE value ISNULL);", + ) + .execute(&mut conn) + .await?; + + detach_db(&mut conn, "patchDb").await +} + +#[cfg(test)] +mod tests { + use sqlx::Executor as _; + + use super::*; + use crate::MbtilesCopier; + + #[actix_rt::test] + async fn apply_flat_patch_file() -> MbtResult<()> { + // Copy the src file to an in-memory DB + let src_file = PathBuf::from("../tests/fixtures/mbtiles/world_cities.mbtiles"); + let src = PathBuf::from("file:apply_flat_diff_file_mem_db?mode=memory&cache=shared"); + + let mut src_conn = MbtilesCopier::new(src_file.clone(), src.clone()) + .run() + .await?; + + // Apply patch to the src data in in-memory DB + let patch_file = PathBuf::from("../tests/fixtures/mbtiles/world_cities_diff.mbtiles"); + apply_patch(src, patch_file).await?; + + // Verify the data is the same as the file the patch was generated from + Mbtiles::new("../tests/fixtures/mbtiles/world_cities_modified.mbtiles")? + .attach_to(&mut src_conn, "testOtherDb") + .await?; + + assert!(src_conn + .fetch_optional("SELECT * FROM tiles EXCEPT SELECT * FROM testOtherDb.tiles;") + .await? + .is_none()); + + Ok(()) + } + + #[actix_rt::test] + async fn apply_normalized_patch_file() -> MbtResult<()> { + // Copy the src file to an in-memory DB + let src_file = PathBuf::from("../tests/fixtures/mbtiles/geography-class-jpg.mbtiles"); + let src = PathBuf::from("file:apply_normalized_diff_file_mem_db?mode=memory&cache=shared"); + + let mut src_conn = MbtilesCopier::new(src_file.clone(), src.clone()) + .run() + .await?; + + // Apply patch to the src data in in-memory DB + let patch_file = + PathBuf::from("../tests/fixtures/mbtiles/geography-class-jpg-diff.mbtiles"); + apply_patch(src, patch_file).await?; + + // Verify the data is the same as the file the patch was generated from + Mbtiles::new("../tests/fixtures/mbtiles/geography-class-jpg-modified.mbtiles")? + .attach_to(&mut src_conn, "testOtherDb") + .await?; + + assert!(src_conn + .fetch_optional("SELECT * FROM tiles EXCEPT SELECT * FROM testOtherDb.tiles;") + .await? + .is_none()); + + Ok(()) + } +} diff --git a/martin-mbtiles/src/queries.rs b/martin-mbtiles/src/queries.rs index 2986dab69..c41531ff5 100644 --- a/martin-mbtiles/src/queries.rs +++ b/martin-mbtiles/src/queries.rs @@ -1,7 +1,19 @@ +use log::debug; use sqlx::{query, Executor as _, SqliteExecutor}; use crate::errors::MbtResult; +/// Returns true if the database is empty (no tables/indexes/...) +pub async fn is_empty_database(conn: &mut T) -> MbtResult +where + for<'e> &'e mut T: SqliteExecutor<'e>, +{ + Ok(query!("SELECT 1 as has_rows FROM sqlite_schema LIMIT 1") + .fetch_optional(&mut *conn) + .await? + .is_none()) +} + pub async fn is_normalized_tables_type(conn: &mut T) -> MbtResult where for<'e> &'e mut T: SqliteExecutor<'e>, @@ -52,20 +64,14 @@ where == 1) } -pub async fn is_flat_with_hash_tables_type(conn: &mut T) -> MbtResult +/// Check if MBTiles has a table or a view named 'tiles_with_hash' with needed fields +pub async fn has_tiles_with_hash(conn: &mut T) -> MbtResult where for<'e> &'e mut T: SqliteExecutor<'e>, { let sql = query!( "SELECT ( - -- Has a 'tiles_with_hash' table - SELECT COUNT(*) = 1 - FROM sqlite_master - WHERE name = 'tiles_with_hash' - AND type = 'table' - -- - ) AND ( - -- 'tiles_with_hash' table's columns and their types are as expected: + -- 'tiles_with_hash' table or view columns and their types are as expected: -- 5 columns (zoom_level, tile_column, tile_row, tile_data, tile_hash). -- The order is not important SELECT COUNT(*) = 5 @@ -87,6 +93,26 @@ where == 1) } +pub async fn is_flat_with_hash_tables_type(conn: &mut T) -> MbtResult +where + for<'e> &'e mut T: SqliteExecutor<'e>, +{ + let sql = query!( + "SELECT ( + -- Has a 'tiles_with_hash' table + SELECT COUNT(*) = 1 + FROM sqlite_master + WHERE name = 'tiles_with_hash' + AND type = 'table' + -- + ) as is_valid;" + ); + + let is_valid = sql.fetch_one(&mut *conn).await?.is_valid; + + Ok(is_valid.unwrap_or_default() == 1 && has_tiles_with_hash(&mut *conn).await?) +} + pub async fn is_flat_tables_type(conn: &mut T) -> MbtResult where for<'e> &'e mut T: SqliteExecutor<'e>, @@ -125,6 +151,7 @@ pub async fn create_metadata_table(conn: &mut T) -> MbtResult<()> where for<'e> &'e mut T: SqliteExecutor<'e>, { + debug!("Creating metadata table if it doesn't already exist"); conn.execute( "CREATE TABLE IF NOT EXISTS metadata ( name text NOT NULL PRIMARY KEY, @@ -141,6 +168,7 @@ where { create_metadata_table(&mut *conn).await?; + debug!("Creating if needed flat table: tiles(z,x,y,data)"); conn.execute( "CREATE TABLE IF NOT EXISTS tiles ( zoom_level integer NOT NULL, @@ -160,6 +188,7 @@ where { create_metadata_table(&mut *conn).await?; + debug!("Creating if needed flat-with-hash table: tiles_with_hash(z,x,y,data,hash)"); conn.execute( "CREATE TABLE IF NOT EXISTS tiles_with_hash ( zoom_level integer NOT NULL, @@ -171,6 +200,7 @@ where ) .await?; + debug!("Creating if needed tiles view for flat-with-hash"); conn.execute( "CREATE VIEW IF NOT EXISTS tiles AS SELECT zoom_level, tile_column, tile_row, tile_data FROM tiles_with_hash;", @@ -186,6 +216,7 @@ where { create_metadata_table(&mut *conn).await?; + debug!("Creating if needed normalized table: map(z,x,y,id)"); conn.execute( "CREATE TABLE IF NOT EXISTS map ( zoom_level integer NOT NULL, @@ -196,13 +227,15 @@ where ) .await?; + debug!("Creating if needed normalized table: images(id,data)"); conn.execute( "CREATE TABLE IF NOT EXISTS images ( - tile_data blob, - tile_id text NOT NULL PRIMARY KEY);", + tile_id text NOT NULL PRIMARY KEY, + tile_data blob);", ) .await?; + debug!("Creating if needed tiles view for flat-with-hash"); conn.execute( "CREATE VIEW IF NOT EXISTS tiles AS SELECT map.zoom_level AS zoom_level, @@ -221,6 +254,7 @@ pub async fn create_tiles_with_hash_view(conn: &mut T) -> MbtResult<()> where for<'e> &'e mut T: SqliteExecutor<'e>, { + debug!("Creating if needed tiles_with_hash view for normalized map+images structure"); conn.execute( "CREATE VIEW IF NOT EXISTS tiles_with_hash AS SELECT @@ -236,3 +270,14 @@ where Ok(()) } + +pub async fn detach_db(conn: &mut T, name: &str) -> MbtResult<()> +where + for<'e> &'e mut T: SqliteExecutor<'e>, +{ + debug!("Detaching {name}"); + query(&format!("DETACH DATABASE {name}")) + .execute(conn) + .await?; + Ok(()) +} diff --git a/martin-mbtiles/tests/mbtiles.rs b/martin-mbtiles/tests/mbtiles.rs new file mode 100644 index 000000000..a152b8613 --- /dev/null +++ b/martin-mbtiles/tests/mbtiles.rs @@ -0,0 +1,409 @@ +use std::collections::HashMap; +use std::path::PathBuf; +use std::str::from_utf8; + +use ctor::ctor; +use insta::{allow_duplicates, assert_display_snapshot}; +use log::info; +use martin_mbtiles::IntegrityCheckType::Off; +use martin_mbtiles::MbtType::{Flat, FlatWithHash, Normalized}; +use martin_mbtiles::{apply_patch, create_flat_tables, MbtResult, MbtType, Mbtiles, MbtilesCopier}; +use pretty_assertions::assert_eq as pretty_assert_eq; +use rstest::{fixture, rstest}; +use serde::Serialize; +use sqlx::{query, query_as, Executor as _, Row, SqliteConnection}; + +const TILES_V1: &str = " + INSERT INTO tiles (zoom_level, tile_column, tile_row, tile_data) VALUES + --(z, x, y, data) -- rules: keep if x=0, edit if x=1, remove if x=2 + (5, 0, 0, cast('same' as blob)) + , (5, 1, 1, cast('edit-v1' as blob)) + , (5, 2, 2, cast('remove' as blob)) + , (6, 0, 3, cast('same' as blob)) + , (6, 1, 4, cast('edit-v1' as blob)) + , (6, 0, 5, cast('1-keep-1-rm' as blob)) + , (6, 2, 6, cast('1-keep-1-rm' as blob)) + ;"; + +const TILES_V2: &str = " + INSERT INTO tiles (zoom_level, tile_column, tile_row, tile_data) VALUES + (5, 0, 0, cast('same' as blob)) -- no changes + , (5, 1, 1, cast('edit-v2' as blob)) -- edited in-place + -- , (5, 2, 2, cast('remove' as blob)) -- this row is deleted + , (6, 0, 3, cast('same' as blob)) -- no changes, same content as 5/0/0 + , (6, 1, 4, cast('edit-v2a' as blob)) -- edited, used to be same as 5/1/1 + , (6, 0, 5, cast('1-keep-1-rm' as blob)) -- this row is kept (same content as next) + -- , (6, 2, 6, cast('1-keep-1-rm' as blob)) -- this row is deleted + , (5, 3, 7, cast('new' as blob)) -- this row is added, dup value + , (5, 3, 8, cast('new' as blob)) -- this row is added, dup value + + -- Expected delta: + -- 5/1/1 edit + -- 5/2/2 remove + -- 5/3/7 add + -- 5/3/8 add + -- 6/1/4 edit + -- 6/2/6 remove + ;"; + +const METADATA_V1: &str = " + INSERT INTO metadata (name, value) VALUES + ('md-same', 'value - same') + , ('md-edit', 'value - v1') + , ('md-remove', 'value - remove') + ;"; + +const METADATA_V2: &str = " + INSERT INTO metadata (name, value) VALUES + ('md-same', 'value - same') + , ('md-edit', 'value - v2') + , ('md-new', 'value - new') + ;"; + +#[ctor] +fn init() { + let _ = env_logger::builder().is_test(true).try_init(); +} + +fn path(mbt: &Mbtiles) -> PathBuf { + PathBuf::from(mbt.filepath()) +} + +fn copier(src: &Mbtiles, dst: &Mbtiles) -> MbtilesCopier { + MbtilesCopier::new(path(src), path(dst)) +} + +fn shorten(v: MbtType) -> &'static str { + match v { + Flat => "flat", + FlatWithHash => "hash", + Normalized => "norm", + } +} + +async fn open(file: &str) -> MbtResult<(Mbtiles, SqliteConnection)> { + let mbtiles = Mbtiles::new(file)?; + let conn = mbtiles.open().await?; + Ok((mbtiles, conn)) +} + +macro_rules! open { + ($function:tt, $($arg:tt)*) => { + open!(@"", $function, $($arg)*) + }; + (@$extra:literal, $function:tt, $($arg:tt)*) => {{ + let file = format!("file:{}_{}{}?mode=memory&cache=shared", stringify!($function), format_args!($($arg)*), $extra); + open(&file).await.unwrap() + }}; +} + +/// Create a new SQLite file of given type without agg_tiles_hash metadata value +macro_rules! new_file_no_hash { + ($function:tt, $dst_type:expr, $sql_meta:expr, $sql_data:expr, $($arg:tt)*) => {{ + new_file!(@true, $function, $dst_type, $sql_meta, $sql_data, $($arg)*) + }}; +} + +/// Create a new SQLite file of type $dst_type with the given metadata and tiles +macro_rules! new_file { + ($function:tt, $dst_type:expr, $sql_meta:expr, $sql_data:expr, $($arg:tt)*) => { + new_file!(@false, $function, $dst_type, $sql_meta, $sql_data, $($arg)*) + }; + + (@$skip_agg:expr, $function:tt, $dst_type:expr, $sql_meta:expr, $sql_data:expr, $($arg:tt)*) => {{ + let (tmp_mbt, mut cn_tmp) = open!(@"temp", $function, $($arg)*); + create_flat_tables(&mut cn_tmp).await.unwrap(); + cn_tmp.execute($sql_data).await.unwrap(); + cn_tmp.execute($sql_meta).await.unwrap(); + + let (dst_mbt, cn_dst) = open!($function, $($arg)*); + let mut opt = copier(&tmp_mbt, &dst_mbt); + opt.dst_type = Some($dst_type); + opt.skip_agg_tiles_hash = $skip_agg; + opt.run().await.unwrap(); + + (dst_mbt, cn_dst) + }}; +} + +macro_rules! assert_snapshot { + ($actual_value:expr, $($arg:tt)*) => {{ + let mut settings = insta::Settings::clone_current(); + settings.set_snapshot_suffix(format!($($arg)*)); + let actual_value = &$actual_value; + settings.bind(|| insta::assert_toml_snapshot!(actual_value)); + }}; +} + +macro_rules! assert_dump { + ($connection:expr, $($arg:tt)*) => {{ + let dmp = dump($connection).await.unwrap(); + assert_snapshot!(&dmp, $($arg)*); + dmp + }}; +} + +type Databases = HashMap<(&'static str, MbtType), Vec>; + +#[fixture] +#[once] +fn databases() -> Databases { + futures::executor::block_on(async { + let mut result = HashMap::new(); + for &mbt_typ in &[Flat, FlatWithHash, Normalized] { + let typ = shorten(mbt_typ); + let (raw_mbt, mut cn) = new_file_no_hash!( + databases, + mbt_typ, + METADATA_V1, + TILES_V1, + "{typ}__v1-no-hash" + ); + let dmp = assert_dump!(&mut cn, "{typ}__v1-no-hash"); + result.insert(("v1_no_hash", mbt_typ), dmp); + + let (v1_mbt, mut v1_cn) = open!(databases, "{typ}__v1"); + copier(&raw_mbt, &v1_mbt).run().await.unwrap(); + let dmp = assert_dump!(&mut v1_cn, "{typ}__v1"); + let hash = v1_mbt.validate(Off, false).await.unwrap(); + allow_duplicates! { + assert_display_snapshot!(hash, @"0063DADF9C78A376418DB0D2B00A5F80"); + } + result.insert(("v1", mbt_typ), dmp); + + let (v2_mbt, mut v2_cn) = + new_file!(databases, mbt_typ, METADATA_V2, TILES_V2, "{typ}__v2"); + let dmp = assert_dump!(&mut v2_cn, "{typ}__v2"); + let hash = v2_mbt.validate(Off, false).await.unwrap(); + allow_duplicates! { + assert_display_snapshot!(hash, @"5C90855D70120501451BDD08CA71341A"); + } + result.insert(("v2", mbt_typ), dmp); + + let (dif_mbt, mut dif_cn) = open!(databases, "{typ}__dif"); + let mut opt = copier(&v1_mbt, &dif_mbt); + opt.diff_with_file = Some(path(&v2_mbt)); + opt.run().await.unwrap(); + let dmp = assert_dump!(&mut dif_cn, "{typ}__dif"); + let hash = dif_mbt.validate(Off, false).await.unwrap(); + allow_duplicates! { + assert_display_snapshot!(hash, @"AB9EE21538C1D28BB357ABB3A45BD6BD"); + } + result.insert(("dif", mbt_typ), dmp); + } + result + }) +} + +#[rstest] +#[trace] +#[actix_rt::test] +async fn convert( + #[values(Flat, FlatWithHash, Normalized)] frm_type: MbtType, + #[values(Flat, FlatWithHash, Normalized)] dst_type: MbtType, + #[notrace] databases: &Databases, +) -> MbtResult<()> { + let (frm, to) = (shorten(frm_type), shorten(dst_type)); + let mem = Mbtiles::new(":memory:")?; + let (frm_mbt, _frm_cn) = new_file!(convert, frm_type, METADATA_V1, TILES_V1, "{frm}-{to}"); + + let mut opt = copier(&frm_mbt, &mem); + opt.dst_type = Some(dst_type); + let dmp = dump(&mut opt.run().await?).await?; + pretty_assert_eq!(databases.get(&("v1", dst_type)).unwrap(), &dmp); + + let mut opt = copier(&frm_mbt, &mem); + opt.dst_type = Some(dst_type); + opt.zoom_levels.insert(6); + let z6only = dump(&mut opt.run().await?).await?; + assert_snapshot!(z6only, "v1__z6__{frm}-{to}"); + + let mut opt = copier(&frm_mbt, &mem); + opt.dst_type = Some(dst_type); + opt.min_zoom = Some(6); + pretty_assert_eq!(&z6only, &dump(&mut opt.run().await?).await?); + + let mut opt = copier(&frm_mbt, &mem); + opt.dst_type = Some(dst_type); + opt.min_zoom = Some(6); + opt.max_zoom = Some(6); + pretty_assert_eq!(&z6only, &dump(&mut opt.run().await?).await?); + + Ok(()) +} + +#[rstest] +#[trace] +#[actix_rt::test] +async fn diff_apply( + #[values(Flat, FlatWithHash, Normalized)] v1_type: MbtType, + #[values(Flat, FlatWithHash, Normalized)] v2_type: MbtType, + #[values(None, Some(Flat), Some(FlatWithHash), Some(Normalized))] dif_type: Option, + #[notrace] databases: &Databases, +) -> MbtResult<()> { + let (v1, v2) = (shorten(v1_type), shorten(v2_type)); + let dif = dif_type.map(shorten).unwrap_or("dflt"); + let prefix = format!("{v2}-{v1}={dif}"); + + let (v1_mbt, _v1_cn) = new_file! {diff_apply, v1_type, METADATA_V1, TILES_V1, "{prefix}__v1"}; + let (v2_mbt, _v2_cn) = new_file! {diff_apply, v2_type, METADATA_V2, TILES_V2, "{prefix}__v2"}; + let (dif_mbt, mut dif_cn) = open!(diff_apply, "{prefix}__dif"); + + info!("TEST: Compare v1 with v2, and copy anything that's different (i.e. mathematically: v2-v1=diff)"); + let mut opt = copier(&v1_mbt, &dif_mbt); + opt.diff_with_file = Some(path(&v2_mbt)); + if let Some(dif_type) = dif_type { + opt.dst_type = Some(dif_type); + } + opt.run().await?; + pretty_assert_eq!( + &dump(&mut dif_cn).await?, + databases + .get(&("dif", dif_type.unwrap_or(v1_type))) + .unwrap() + ); + + for target_type in &[Flat, FlatWithHash, Normalized] { + let trg = shorten(*target_type); + let prefix = format!("{prefix}__to__{trg}"); + let expected_v2 = databases.get(&("v2", *target_type)).unwrap(); + + info!("TEST: Applying the difference (v2-v1=diff) to v1, should get v2"); + let (tar1_mbt, mut tar1_cn) = + new_file! {diff_apply, *target_type, METADATA_V1, TILES_V1, "{prefix}__v1"}; + apply_patch(path(&tar1_mbt), path(&dif_mbt)).await?; + let hash_v1 = tar1_mbt.validate(Off, false).await?; + allow_duplicates! { + assert_display_snapshot!(hash_v1, @"5C90855D70120501451BDD08CA71341A"); + } + let dmp = dump(&mut tar1_cn).await?; + pretty_assert_eq!(&dmp, expected_v2); + + info!("TEST: Applying the difference (v2-v1=diff) to v2, should not modify it"); + let (tar2_mbt, mut tar2_cn) = + new_file! {diff_apply, *target_type, METADATA_V2, TILES_V2, "{prefix}__v2"}; + apply_patch(path(&tar2_mbt), path(&dif_mbt)).await?; + let hash_v2 = tar2_mbt.validate(Off, false).await?; + allow_duplicates! { + assert_display_snapshot!(hash_v2, @"5C90855D70120501451BDD08CA71341A"); + } + let dmp = dump(&mut tar2_cn).await?; + pretty_assert_eq!(&dmp, expected_v2); + } + + Ok(()) +} + +// /// A simple tester to run specific values +// #[actix_rt::test] +// async fn test_one() { +// let dif_type = FlatWithHash; +// let src_type = Flat; +// let dst_type = Some(Normalized); +// let db = databases(); +// +// diff_apply(src_type, dif_type, dst_type, &db).await.unwrap(); +// panic!() +// } + +#[derive(Debug, sqlx::FromRow, Serialize, PartialEq)] +struct SqliteEntry { + pub r#type: Option, + pub tbl_name: Option, + pub sql: Option, + #[sqlx(skip)] + pub values: Option>, +} + +async fn dump(conn: &mut SqliteConnection) -> MbtResult> { + let mut result = Vec::new(); + + let schema: Vec = query_as( + "SELECT type, tbl_name, sql + FROM sqlite_schema + ORDER BY type != 'table', type, tbl_name", + ) + .fetch_all(&mut *conn) + .await?; + + for mut entry in schema { + let tbl = match (&entry.r#type, &entry.tbl_name) { + (Some(typ), Some(tbl)) if typ == "table" => tbl, + _ => { + result.push(entry); + continue; + } + }; + + let sql = format!("PRAGMA table_info({tbl})"); + let columns: Vec<_> = query(&sql) + .fetch_all(&mut *conn) + .await? + .into_iter() + .map(|row| { + let cid: i32 = row.get(0); + let typ: String = row.get(2); + (cid as usize, typ) + }) + .collect(); + + let sql = format!("SELECT * FROM {tbl}"); + let rows = query(&sql).fetch_all(&mut *conn).await?; + let mut values = rows + .iter() + .map(|row| { + let val = columns + .iter() + .map(|(idx, typ)| { + // use sqlx::ValueRef as _; + // let raw = row.try_get_raw(*idx).unwrap(); + // if raw.is_null() { + // return "NULL".to_string(); + // } + // if let Ok(v) = row.try_get::(idx) { + // return format!(r#""{v}""#); + // } + // if let Ok(v) = row.try_get::, _>(idx) { + // return format!("blob({})", from_utf8(&v).unwrap()); + // } + // if let Ok(v) = row.try_get::(idx) { + // return v.to_string(); + // } + // if let Ok(v) = row.try_get::(idx) { + // return v.to_string(); + // } + // panic!("Unknown column type: {typ}"); + (match typ.as_str() { + "INTEGER" => row.get::, _>(idx).map(|v| v.to_string()), + "REAL" => row.get::, _>(idx).map(|v| v.to_string()), + "TEXT" => row + .get::, _>(idx) + .map(|v| format!(r#""{v}""#)), + "BLOB" => row + .get::>, _>(idx) + .map(|v| format!("blob({})", from_utf8(&v).unwrap())), + _ => panic!("Unknown column type: {typ}"), + }) + .unwrap_or("NULL".to_string()) + }) + .collect::>() + .join(", "); + format!("( {val} )") + }) + .collect::>(); + + values.sort(); + entry.values = Some(values); + result.push(entry); + } + + Ok(result) +} + +#[allow(dead_code)] +async fn save_to_file(source_mbt: &Mbtiles, path: &str) -> MbtResult<()> { + let mut opt = copier(source_mbt, &Mbtiles::new(path)?); + opt.skip_agg_tiles_hash = true; + opt.run().await?; + Ok(()) +} diff --git a/martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__flat-flat.snap b/martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__flat-flat.snap new file mode 100644 index 000000000..f0a89da1d --- /dev/null +++ b/martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__flat-flat.snap @@ -0,0 +1,42 @@ +--- +source: martin-mbtiles/tests/mbtiles.rs +expression: actual_value +--- +[[]] +type = 'table' +tbl_name = 'metadata' +sql = ''' +CREATE TABLE metadata ( + name text NOT NULL PRIMARY KEY, + value text)''' +values = [ + '( "agg_tiles_hash", "675349A4153AEC0679BE9C0637AEEBCC" )', + '( "md-edit", "value - v1" )', + '( "md-remove", "value - remove" )', + '( "md-same", "value - same" )', +] + +[[]] +type = 'table' +tbl_name = 'tiles' +sql = ''' +CREATE TABLE tiles ( + zoom_level integer NOT NULL, + tile_column integer NOT NULL, + tile_row integer NOT NULL, + tile_data blob, + PRIMARY KEY(zoom_level, tile_column, tile_row))''' +values = [ + '( 6, 0, 3, blob(same) )', + '( 6, 0, 5, blob(1-keep-1-rm) )', + '( 6, 1, 4, blob(edit-v1) )', + '( 6, 2, 6, blob(1-keep-1-rm) )', +] + +[[]] +type = 'index' +tbl_name = 'metadata' + +[[]] +type = 'index' +tbl_name = 'tiles' diff --git a/martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__flat-hash.snap b/martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__flat-hash.snap new file mode 100644 index 000000000..19aa43b08 --- /dev/null +++ b/martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__flat-hash.snap @@ -0,0 +1,50 @@ +--- +source: martin-mbtiles/tests/mbtiles.rs +expression: actual_value +--- +[[]] +type = 'table' +tbl_name = 'metadata' +sql = ''' +CREATE TABLE metadata ( + name text NOT NULL PRIMARY KEY, + value text)''' +values = [ + '( "agg_tiles_hash", "675349A4153AEC0679BE9C0637AEEBCC" )', + '( "md-edit", "value - v1" )', + '( "md-remove", "value - remove" )', + '( "md-same", "value - same" )', +] + +[[]] +type = 'table' +tbl_name = 'tiles_with_hash' +sql = ''' +CREATE TABLE tiles_with_hash ( + zoom_level integer NOT NULL, + tile_column integer NOT NULL, + tile_row integer NOT NULL, + tile_data blob, + tile_hash text, + PRIMARY KEY(zoom_level, tile_column, tile_row))''' +values = [ + '( 6, 0, 3, blob(same), "51037A4A37730F52C8732586D3AAA316" )', + '( 6, 0, 5, blob(1-keep-1-rm), "535A5575B48444EDEB926815AB26EC9B" )', + '( 6, 1, 4, blob(edit-v1), "EFE0AE5FD114DE99855BC2838BE97E1D" )', + '( 6, 2, 6, blob(1-keep-1-rm), "535A5575B48444EDEB926815AB26EC9B" )', +] + +[[]] +type = 'index' +tbl_name = 'metadata' + +[[]] +type = 'index' +tbl_name = 'tiles_with_hash' + +[[]] +type = 'view' +tbl_name = 'tiles' +sql = ''' +CREATE VIEW tiles AS + SELECT zoom_level, tile_column, tile_row, tile_data FROM tiles_with_hash''' diff --git a/martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__flat-norm.snap b/martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__flat-norm.snap new file mode 100644 index 000000000..d129463dd --- /dev/null +++ b/martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__flat-norm.snap @@ -0,0 +1,85 @@ +--- +source: martin-mbtiles/tests/mbtiles.rs +expression: actual_value +--- +[[]] +type = 'table' +tbl_name = 'images' +sql = ''' +CREATE TABLE images ( + tile_id text NOT NULL PRIMARY KEY, + tile_data blob)''' +values = [ + '( "51037A4A37730F52C8732586D3AAA316", blob(same) )', + '( "535A5575B48444EDEB926815AB26EC9B", blob(1-keep-1-rm) )', + '( "EFE0AE5FD114DE99855BC2838BE97E1D", blob(edit-v1) )', +] + +[[]] +type = 'table' +tbl_name = 'map' +sql = ''' +CREATE TABLE map ( + zoom_level integer NOT NULL, + tile_column integer NOT NULL, + tile_row integer NOT NULL, + tile_id text, + PRIMARY KEY(zoom_level, tile_column, tile_row))''' +values = [ + '( 6, 0, 3, "51037A4A37730F52C8732586D3AAA316" )', + '( 6, 0, 5, "535A5575B48444EDEB926815AB26EC9B" )', + '( 6, 1, 4, "EFE0AE5FD114DE99855BC2838BE97E1D" )', + '( 6, 2, 6, "535A5575B48444EDEB926815AB26EC9B" )', +] + +[[]] +type = 'table' +tbl_name = 'metadata' +sql = ''' +CREATE TABLE metadata ( + name text NOT NULL PRIMARY KEY, + value text)''' +values = [ + '( "agg_tiles_hash", "675349A4153AEC0679BE9C0637AEEBCC" )', + '( "md-edit", "value - v1" )', + '( "md-remove", "value - remove" )', + '( "md-same", "value - same" )', +] + +[[]] +type = 'index' +tbl_name = 'images' + +[[]] +type = 'index' +tbl_name = 'map' + +[[]] +type = 'index' +tbl_name = 'metadata' + +[[]] +type = 'view' +tbl_name = 'tiles' +sql = ''' +CREATE VIEW tiles AS + SELECT map.zoom_level AS zoom_level, + map.tile_column AS tile_column, + map.tile_row AS tile_row, + images.tile_data AS tile_data + FROM map + JOIN images ON images.tile_id = map.tile_id''' + +[[]] +type = 'view' +tbl_name = 'tiles_with_hash' +sql = ''' +CREATE VIEW tiles_with_hash AS + SELECT + map.zoom_level AS zoom_level, + map.tile_column AS tile_column, + map.tile_row AS tile_row, + images.tile_data AS tile_data, + images.tile_id AS tile_hash + FROM map + JOIN images ON images.tile_id = map.tile_id''' diff --git a/martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__hash-flat.snap b/martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__hash-flat.snap new file mode 100644 index 000000000..f0a89da1d --- /dev/null +++ b/martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__hash-flat.snap @@ -0,0 +1,42 @@ +--- +source: martin-mbtiles/tests/mbtiles.rs +expression: actual_value +--- +[[]] +type = 'table' +tbl_name = 'metadata' +sql = ''' +CREATE TABLE metadata ( + name text NOT NULL PRIMARY KEY, + value text)''' +values = [ + '( "agg_tiles_hash", "675349A4153AEC0679BE9C0637AEEBCC" )', + '( "md-edit", "value - v1" )', + '( "md-remove", "value - remove" )', + '( "md-same", "value - same" )', +] + +[[]] +type = 'table' +tbl_name = 'tiles' +sql = ''' +CREATE TABLE tiles ( + zoom_level integer NOT NULL, + tile_column integer NOT NULL, + tile_row integer NOT NULL, + tile_data blob, + PRIMARY KEY(zoom_level, tile_column, tile_row))''' +values = [ + '( 6, 0, 3, blob(same) )', + '( 6, 0, 5, blob(1-keep-1-rm) )', + '( 6, 1, 4, blob(edit-v1) )', + '( 6, 2, 6, blob(1-keep-1-rm) )', +] + +[[]] +type = 'index' +tbl_name = 'metadata' + +[[]] +type = 'index' +tbl_name = 'tiles' diff --git a/martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__hash-hash.snap b/martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__hash-hash.snap new file mode 100644 index 000000000..19aa43b08 --- /dev/null +++ b/martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__hash-hash.snap @@ -0,0 +1,50 @@ +--- +source: martin-mbtiles/tests/mbtiles.rs +expression: actual_value +--- +[[]] +type = 'table' +tbl_name = 'metadata' +sql = ''' +CREATE TABLE metadata ( + name text NOT NULL PRIMARY KEY, + value text)''' +values = [ + '( "agg_tiles_hash", "675349A4153AEC0679BE9C0637AEEBCC" )', + '( "md-edit", "value - v1" )', + '( "md-remove", "value - remove" )', + '( "md-same", "value - same" )', +] + +[[]] +type = 'table' +tbl_name = 'tiles_with_hash' +sql = ''' +CREATE TABLE tiles_with_hash ( + zoom_level integer NOT NULL, + tile_column integer NOT NULL, + tile_row integer NOT NULL, + tile_data blob, + tile_hash text, + PRIMARY KEY(zoom_level, tile_column, tile_row))''' +values = [ + '( 6, 0, 3, blob(same), "51037A4A37730F52C8732586D3AAA316" )', + '( 6, 0, 5, blob(1-keep-1-rm), "535A5575B48444EDEB926815AB26EC9B" )', + '( 6, 1, 4, blob(edit-v1), "EFE0AE5FD114DE99855BC2838BE97E1D" )', + '( 6, 2, 6, blob(1-keep-1-rm), "535A5575B48444EDEB926815AB26EC9B" )', +] + +[[]] +type = 'index' +tbl_name = 'metadata' + +[[]] +type = 'index' +tbl_name = 'tiles_with_hash' + +[[]] +type = 'view' +tbl_name = 'tiles' +sql = ''' +CREATE VIEW tiles AS + SELECT zoom_level, tile_column, tile_row, tile_data FROM tiles_with_hash''' diff --git a/martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__hash-norm.snap b/martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__hash-norm.snap new file mode 100644 index 000000000..d129463dd --- /dev/null +++ b/martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__hash-norm.snap @@ -0,0 +1,85 @@ +--- +source: martin-mbtiles/tests/mbtiles.rs +expression: actual_value +--- +[[]] +type = 'table' +tbl_name = 'images' +sql = ''' +CREATE TABLE images ( + tile_id text NOT NULL PRIMARY KEY, + tile_data blob)''' +values = [ + '( "51037A4A37730F52C8732586D3AAA316", blob(same) )', + '( "535A5575B48444EDEB926815AB26EC9B", blob(1-keep-1-rm) )', + '( "EFE0AE5FD114DE99855BC2838BE97E1D", blob(edit-v1) )', +] + +[[]] +type = 'table' +tbl_name = 'map' +sql = ''' +CREATE TABLE map ( + zoom_level integer NOT NULL, + tile_column integer NOT NULL, + tile_row integer NOT NULL, + tile_id text, + PRIMARY KEY(zoom_level, tile_column, tile_row))''' +values = [ + '( 6, 0, 3, "51037A4A37730F52C8732586D3AAA316" )', + '( 6, 0, 5, "535A5575B48444EDEB926815AB26EC9B" )', + '( 6, 1, 4, "EFE0AE5FD114DE99855BC2838BE97E1D" )', + '( 6, 2, 6, "535A5575B48444EDEB926815AB26EC9B" )', +] + +[[]] +type = 'table' +tbl_name = 'metadata' +sql = ''' +CREATE TABLE metadata ( + name text NOT NULL PRIMARY KEY, + value text)''' +values = [ + '( "agg_tiles_hash", "675349A4153AEC0679BE9C0637AEEBCC" )', + '( "md-edit", "value - v1" )', + '( "md-remove", "value - remove" )', + '( "md-same", "value - same" )', +] + +[[]] +type = 'index' +tbl_name = 'images' + +[[]] +type = 'index' +tbl_name = 'map' + +[[]] +type = 'index' +tbl_name = 'metadata' + +[[]] +type = 'view' +tbl_name = 'tiles' +sql = ''' +CREATE VIEW tiles AS + SELECT map.zoom_level AS zoom_level, + map.tile_column AS tile_column, + map.tile_row AS tile_row, + images.tile_data AS tile_data + FROM map + JOIN images ON images.tile_id = map.tile_id''' + +[[]] +type = 'view' +tbl_name = 'tiles_with_hash' +sql = ''' +CREATE VIEW tiles_with_hash AS + SELECT + map.zoom_level AS zoom_level, + map.tile_column AS tile_column, + map.tile_row AS tile_row, + images.tile_data AS tile_data, + images.tile_id AS tile_hash + FROM map + JOIN images ON images.tile_id = map.tile_id''' diff --git a/martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__norm-flat.snap b/martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__norm-flat.snap new file mode 100644 index 000000000..f0a89da1d --- /dev/null +++ b/martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__norm-flat.snap @@ -0,0 +1,42 @@ +--- +source: martin-mbtiles/tests/mbtiles.rs +expression: actual_value +--- +[[]] +type = 'table' +tbl_name = 'metadata' +sql = ''' +CREATE TABLE metadata ( + name text NOT NULL PRIMARY KEY, + value text)''' +values = [ + '( "agg_tiles_hash", "675349A4153AEC0679BE9C0637AEEBCC" )', + '( "md-edit", "value - v1" )', + '( "md-remove", "value - remove" )', + '( "md-same", "value - same" )', +] + +[[]] +type = 'table' +tbl_name = 'tiles' +sql = ''' +CREATE TABLE tiles ( + zoom_level integer NOT NULL, + tile_column integer NOT NULL, + tile_row integer NOT NULL, + tile_data blob, + PRIMARY KEY(zoom_level, tile_column, tile_row))''' +values = [ + '( 6, 0, 3, blob(same) )', + '( 6, 0, 5, blob(1-keep-1-rm) )', + '( 6, 1, 4, blob(edit-v1) )', + '( 6, 2, 6, blob(1-keep-1-rm) )', +] + +[[]] +type = 'index' +tbl_name = 'metadata' + +[[]] +type = 'index' +tbl_name = 'tiles' diff --git a/martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__norm-hash.snap b/martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__norm-hash.snap new file mode 100644 index 000000000..19aa43b08 --- /dev/null +++ b/martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__norm-hash.snap @@ -0,0 +1,50 @@ +--- +source: martin-mbtiles/tests/mbtiles.rs +expression: actual_value +--- +[[]] +type = 'table' +tbl_name = 'metadata' +sql = ''' +CREATE TABLE metadata ( + name text NOT NULL PRIMARY KEY, + value text)''' +values = [ + '( "agg_tiles_hash", "675349A4153AEC0679BE9C0637AEEBCC" )', + '( "md-edit", "value - v1" )', + '( "md-remove", "value - remove" )', + '( "md-same", "value - same" )', +] + +[[]] +type = 'table' +tbl_name = 'tiles_with_hash' +sql = ''' +CREATE TABLE tiles_with_hash ( + zoom_level integer NOT NULL, + tile_column integer NOT NULL, + tile_row integer NOT NULL, + tile_data blob, + tile_hash text, + PRIMARY KEY(zoom_level, tile_column, tile_row))''' +values = [ + '( 6, 0, 3, blob(same), "51037A4A37730F52C8732586D3AAA316" )', + '( 6, 0, 5, blob(1-keep-1-rm), "535A5575B48444EDEB926815AB26EC9B" )', + '( 6, 1, 4, blob(edit-v1), "EFE0AE5FD114DE99855BC2838BE97E1D" )', + '( 6, 2, 6, blob(1-keep-1-rm), "535A5575B48444EDEB926815AB26EC9B" )', +] + +[[]] +type = 'index' +tbl_name = 'metadata' + +[[]] +type = 'index' +tbl_name = 'tiles_with_hash' + +[[]] +type = 'view' +tbl_name = 'tiles' +sql = ''' +CREATE VIEW tiles AS + SELECT zoom_level, tile_column, tile_row, tile_data FROM tiles_with_hash''' diff --git a/martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__norm-norm.snap b/martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__norm-norm.snap new file mode 100644 index 000000000..d129463dd --- /dev/null +++ b/martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__norm-norm.snap @@ -0,0 +1,85 @@ +--- +source: martin-mbtiles/tests/mbtiles.rs +expression: actual_value +--- +[[]] +type = 'table' +tbl_name = 'images' +sql = ''' +CREATE TABLE images ( + tile_id text NOT NULL PRIMARY KEY, + tile_data blob)''' +values = [ + '( "51037A4A37730F52C8732586D3AAA316", blob(same) )', + '( "535A5575B48444EDEB926815AB26EC9B", blob(1-keep-1-rm) )', + '( "EFE0AE5FD114DE99855BC2838BE97E1D", blob(edit-v1) )', +] + +[[]] +type = 'table' +tbl_name = 'map' +sql = ''' +CREATE TABLE map ( + zoom_level integer NOT NULL, + tile_column integer NOT NULL, + tile_row integer NOT NULL, + tile_id text, + PRIMARY KEY(zoom_level, tile_column, tile_row))''' +values = [ + '( 6, 0, 3, "51037A4A37730F52C8732586D3AAA316" )', + '( 6, 0, 5, "535A5575B48444EDEB926815AB26EC9B" )', + '( 6, 1, 4, "EFE0AE5FD114DE99855BC2838BE97E1D" )', + '( 6, 2, 6, "535A5575B48444EDEB926815AB26EC9B" )', +] + +[[]] +type = 'table' +tbl_name = 'metadata' +sql = ''' +CREATE TABLE metadata ( + name text NOT NULL PRIMARY KEY, + value text)''' +values = [ + '( "agg_tiles_hash", "675349A4153AEC0679BE9C0637AEEBCC" )', + '( "md-edit", "value - v1" )', + '( "md-remove", "value - remove" )', + '( "md-same", "value - same" )', +] + +[[]] +type = 'index' +tbl_name = 'images' + +[[]] +type = 'index' +tbl_name = 'map' + +[[]] +type = 'index' +tbl_name = 'metadata' + +[[]] +type = 'view' +tbl_name = 'tiles' +sql = ''' +CREATE VIEW tiles AS + SELECT map.zoom_level AS zoom_level, + map.tile_column AS tile_column, + map.tile_row AS tile_row, + images.tile_data AS tile_data + FROM map + JOIN images ON images.tile_id = map.tile_id''' + +[[]] +type = 'view' +tbl_name = 'tiles_with_hash' +sql = ''' +CREATE VIEW tiles_with_hash AS + SELECT + map.zoom_level AS zoom_level, + map.tile_column AS tile_column, + map.tile_row AS tile_row, + images.tile_data AS tile_data, + images.tile_id AS tile_hash + FROM map + JOIN images ON images.tile_id = map.tile_id''' diff --git a/martin-mbtiles/tests/snapshots/mbtiles__databases@flat__dif.snap b/martin-mbtiles/tests/snapshots/mbtiles__databases@flat__dif.snap new file mode 100644 index 000000000..e39c786f7 --- /dev/null +++ b/martin-mbtiles/tests/snapshots/mbtiles__databases@flat__dif.snap @@ -0,0 +1,45 @@ +--- +source: martin-mbtiles/tests/mbtiles.rs +expression: actual_value +--- +[[]] +type = 'table' +tbl_name = 'metadata' +sql = ''' +CREATE TABLE metadata ( + name text NOT NULL PRIMARY KEY, + value text)''' +values = [ + '( "agg_tiles_hash", "AB9EE21538C1D28BB357ABB3A45BD6BD" )', + '( "agg_tiles_hash_after_apply", "5C90855D70120501451BDD08CA71341A" )', + '( "md-edit", "value - v2" )', + '( "md-new", "value - new" )', + '( "md-remove", NULL )', +] + +[[]] +type = 'table' +tbl_name = 'tiles' +sql = ''' +CREATE TABLE tiles ( + zoom_level integer NOT NULL, + tile_column integer NOT NULL, + tile_row integer NOT NULL, + tile_data blob, + PRIMARY KEY(zoom_level, tile_column, tile_row))''' +values = [ + '( 5, 1, 1, blob(edit-v2) )', + '( 5, 2, 2, NULL )', + '( 5, 3, 7, blob(new) )', + '( 5, 3, 8, blob(new) )', + '( 6, 1, 4, blob(edit-v2a) )', + '( 6, 2, 6, NULL )', +] + +[[]] +type = 'index' +tbl_name = 'metadata' + +[[]] +type = 'index' +tbl_name = 'tiles' diff --git a/martin-mbtiles/tests/snapshots/mbtiles__databases@flat__v1-no-hash.snap b/martin-mbtiles/tests/snapshots/mbtiles__databases@flat__v1-no-hash.snap new file mode 100644 index 000000000..c0ede5eaa --- /dev/null +++ b/martin-mbtiles/tests/snapshots/mbtiles__databases@flat__v1-no-hash.snap @@ -0,0 +1,44 @@ +--- +source: martin-mbtiles/tests/mbtiles.rs +expression: actual_value +--- +[[]] +type = 'table' +tbl_name = 'metadata' +sql = ''' +CREATE TABLE metadata ( + name text NOT NULL PRIMARY KEY, + value text)''' +values = [ + '( "md-edit", "value - v1" )', + '( "md-remove", "value - remove" )', + '( "md-same", "value - same" )', +] + +[[]] +type = 'table' +tbl_name = 'tiles' +sql = ''' +CREATE TABLE tiles ( + zoom_level integer NOT NULL, + tile_column integer NOT NULL, + tile_row integer NOT NULL, + tile_data blob, + PRIMARY KEY(zoom_level, tile_column, tile_row))''' +values = [ + '( 5, 0, 0, blob(same) )', + '( 5, 1, 1, blob(edit-v1) )', + '( 5, 2, 2, blob(remove) )', + '( 6, 0, 3, blob(same) )', + '( 6, 0, 5, blob(1-keep-1-rm) )', + '( 6, 1, 4, blob(edit-v1) )', + '( 6, 2, 6, blob(1-keep-1-rm) )', +] + +[[]] +type = 'index' +tbl_name = 'metadata' + +[[]] +type = 'index' +tbl_name = 'tiles' diff --git a/martin-mbtiles/tests/snapshots/mbtiles__databases@flat__v1.snap b/martin-mbtiles/tests/snapshots/mbtiles__databases@flat__v1.snap new file mode 100644 index 000000000..48b9f31cf --- /dev/null +++ b/martin-mbtiles/tests/snapshots/mbtiles__databases@flat__v1.snap @@ -0,0 +1,45 @@ +--- +source: martin-mbtiles/tests/mbtiles.rs +expression: actual_value +--- +[[]] +type = 'table' +tbl_name = 'metadata' +sql = ''' +CREATE TABLE metadata ( + name text NOT NULL PRIMARY KEY, + value text)''' +values = [ + '( "agg_tiles_hash", "0063DADF9C78A376418DB0D2B00A5F80" )', + '( "md-edit", "value - v1" )', + '( "md-remove", "value - remove" )', + '( "md-same", "value - same" )', +] + +[[]] +type = 'table' +tbl_name = 'tiles' +sql = ''' +CREATE TABLE tiles ( + zoom_level integer NOT NULL, + tile_column integer NOT NULL, + tile_row integer NOT NULL, + tile_data blob, + PRIMARY KEY(zoom_level, tile_column, tile_row))''' +values = [ + '( 5, 0, 0, blob(same) )', + '( 5, 1, 1, blob(edit-v1) )', + '( 5, 2, 2, blob(remove) )', + '( 6, 0, 3, blob(same) )', + '( 6, 0, 5, blob(1-keep-1-rm) )', + '( 6, 1, 4, blob(edit-v1) )', + '( 6, 2, 6, blob(1-keep-1-rm) )', +] + +[[]] +type = 'index' +tbl_name = 'metadata' + +[[]] +type = 'index' +tbl_name = 'tiles' diff --git a/martin-mbtiles/tests/snapshots/mbtiles__databases@flat__v2.snap b/martin-mbtiles/tests/snapshots/mbtiles__databases@flat__v2.snap new file mode 100644 index 000000000..e316a7461 --- /dev/null +++ b/martin-mbtiles/tests/snapshots/mbtiles__databases@flat__v2.snap @@ -0,0 +1,45 @@ +--- +source: martin-mbtiles/tests/mbtiles.rs +expression: actual_value +--- +[[]] +type = 'table' +tbl_name = 'metadata' +sql = ''' +CREATE TABLE metadata ( + name text NOT NULL PRIMARY KEY, + value text)''' +values = [ + '( "agg_tiles_hash", "5C90855D70120501451BDD08CA71341A" )', + '( "md-edit", "value - v2" )', + '( "md-new", "value - new" )', + '( "md-same", "value - same" )', +] + +[[]] +type = 'table' +tbl_name = 'tiles' +sql = ''' +CREATE TABLE tiles ( + zoom_level integer NOT NULL, + tile_column integer NOT NULL, + tile_row integer NOT NULL, + tile_data blob, + PRIMARY KEY(zoom_level, tile_column, tile_row))''' +values = [ + '( 5, 0, 0, blob(same) )', + '( 5, 1, 1, blob(edit-v2) )', + '( 5, 3, 7, blob(new) )', + '( 5, 3, 8, blob(new) )', + '( 6, 0, 3, blob(same) )', + '( 6, 0, 5, blob(1-keep-1-rm) )', + '( 6, 1, 4, blob(edit-v2a) )', +] + +[[]] +type = 'index' +tbl_name = 'metadata' + +[[]] +type = 'index' +tbl_name = 'tiles' diff --git a/martin-mbtiles/tests/snapshots/mbtiles__databases@hash__dif.snap b/martin-mbtiles/tests/snapshots/mbtiles__databases@hash__dif.snap new file mode 100644 index 000000000..e3dcecf0c --- /dev/null +++ b/martin-mbtiles/tests/snapshots/mbtiles__databases@hash__dif.snap @@ -0,0 +1,53 @@ +--- +source: martin-mbtiles/tests/mbtiles.rs +expression: actual_value +--- +[[]] +type = 'table' +tbl_name = 'metadata' +sql = ''' +CREATE TABLE metadata ( + name text NOT NULL PRIMARY KEY, + value text)''' +values = [ + '( "agg_tiles_hash", "AB9EE21538C1D28BB357ABB3A45BD6BD" )', + '( "agg_tiles_hash_after_apply", "5C90855D70120501451BDD08CA71341A" )', + '( "md-edit", "value - v2" )', + '( "md-new", "value - new" )', + '( "md-remove", NULL )', +] + +[[]] +type = 'table' +tbl_name = 'tiles_with_hash' +sql = ''' +CREATE TABLE tiles_with_hash ( + zoom_level integer NOT NULL, + tile_column integer NOT NULL, + tile_row integer NOT NULL, + tile_data blob, + tile_hash text, + PRIMARY KEY(zoom_level, tile_column, tile_row))''' +values = [ + '( 5, 1, 1, blob(edit-v2), "FF76830FF90D79BB335884F256031731" )', + '( 5, 2, 2, NULL, "" )', + '( 5, 3, 7, blob(new), "22AF645D1859CB5CA6DA0C484F1F37EA" )', + '( 5, 3, 8, blob(new), "22AF645D1859CB5CA6DA0C484F1F37EA" )', + '( 6, 1, 4, blob(edit-v2a), "03132BFACDB00CC63D6B7DD98D974DD5" )', + '( 6, 2, 6, NULL, "" )', +] + +[[]] +type = 'index' +tbl_name = 'metadata' + +[[]] +type = 'index' +tbl_name = 'tiles_with_hash' + +[[]] +type = 'view' +tbl_name = 'tiles' +sql = ''' +CREATE VIEW tiles AS + SELECT zoom_level, tile_column, tile_row, tile_data FROM tiles_with_hash''' diff --git a/martin-mbtiles/tests/snapshots/mbtiles__databases@hash__v1-no-hash.snap b/martin-mbtiles/tests/snapshots/mbtiles__databases@hash__v1-no-hash.snap new file mode 100644 index 000000000..1c18d1329 --- /dev/null +++ b/martin-mbtiles/tests/snapshots/mbtiles__databases@hash__v1-no-hash.snap @@ -0,0 +1,52 @@ +--- +source: martin-mbtiles/tests/mbtiles.rs +expression: actual_value +--- +[[]] +type = 'table' +tbl_name = 'metadata' +sql = ''' +CREATE TABLE metadata ( + name text NOT NULL PRIMARY KEY, + value text)''' +values = [ + '( "md-edit", "value - v1" )', + '( "md-remove", "value - remove" )', + '( "md-same", "value - same" )', +] + +[[]] +type = 'table' +tbl_name = 'tiles_with_hash' +sql = ''' +CREATE TABLE tiles_with_hash ( + zoom_level integer NOT NULL, + tile_column integer NOT NULL, + tile_row integer NOT NULL, + tile_data blob, + tile_hash text, + PRIMARY KEY(zoom_level, tile_column, tile_row))''' +values = [ + '( 5, 0, 0, blob(same), "51037A4A37730F52C8732586D3AAA316" )', + '( 5, 1, 1, blob(edit-v1), "EFE0AE5FD114DE99855BC2838BE97E1D" )', + '( 5, 2, 2, blob(remove), "0F6969D7052DA9261E31DDB6E88C136E" )', + '( 6, 0, 3, blob(same), "51037A4A37730F52C8732586D3AAA316" )', + '( 6, 0, 5, blob(1-keep-1-rm), "535A5575B48444EDEB926815AB26EC9B" )', + '( 6, 1, 4, blob(edit-v1), "EFE0AE5FD114DE99855BC2838BE97E1D" )', + '( 6, 2, 6, blob(1-keep-1-rm), "535A5575B48444EDEB926815AB26EC9B" )', +] + +[[]] +type = 'index' +tbl_name = 'metadata' + +[[]] +type = 'index' +tbl_name = 'tiles_with_hash' + +[[]] +type = 'view' +tbl_name = 'tiles' +sql = ''' +CREATE VIEW tiles AS + SELECT zoom_level, tile_column, tile_row, tile_data FROM tiles_with_hash''' diff --git a/martin-mbtiles/tests/snapshots/mbtiles__databases@hash__v1.snap b/martin-mbtiles/tests/snapshots/mbtiles__databases@hash__v1.snap new file mode 100644 index 000000000..bce478b04 --- /dev/null +++ b/martin-mbtiles/tests/snapshots/mbtiles__databases@hash__v1.snap @@ -0,0 +1,53 @@ +--- +source: martin-mbtiles/tests/mbtiles.rs +expression: actual_value +--- +[[]] +type = 'table' +tbl_name = 'metadata' +sql = ''' +CREATE TABLE metadata ( + name text NOT NULL PRIMARY KEY, + value text)''' +values = [ + '( "agg_tiles_hash", "0063DADF9C78A376418DB0D2B00A5F80" )', + '( "md-edit", "value - v1" )', + '( "md-remove", "value - remove" )', + '( "md-same", "value - same" )', +] + +[[]] +type = 'table' +tbl_name = 'tiles_with_hash' +sql = ''' +CREATE TABLE tiles_with_hash ( + zoom_level integer NOT NULL, + tile_column integer NOT NULL, + tile_row integer NOT NULL, + tile_data blob, + tile_hash text, + PRIMARY KEY(zoom_level, tile_column, tile_row))''' +values = [ + '( 5, 0, 0, blob(same), "51037A4A37730F52C8732586D3AAA316" )', + '( 5, 1, 1, blob(edit-v1), "EFE0AE5FD114DE99855BC2838BE97E1D" )', + '( 5, 2, 2, blob(remove), "0F6969D7052DA9261E31DDB6E88C136E" )', + '( 6, 0, 3, blob(same), "51037A4A37730F52C8732586D3AAA316" )', + '( 6, 0, 5, blob(1-keep-1-rm), "535A5575B48444EDEB926815AB26EC9B" )', + '( 6, 1, 4, blob(edit-v1), "EFE0AE5FD114DE99855BC2838BE97E1D" )', + '( 6, 2, 6, blob(1-keep-1-rm), "535A5575B48444EDEB926815AB26EC9B" )', +] + +[[]] +type = 'index' +tbl_name = 'metadata' + +[[]] +type = 'index' +tbl_name = 'tiles_with_hash' + +[[]] +type = 'view' +tbl_name = 'tiles' +sql = ''' +CREATE VIEW tiles AS + SELECT zoom_level, tile_column, tile_row, tile_data FROM tiles_with_hash''' diff --git a/martin-mbtiles/tests/snapshots/mbtiles__databases@hash__v2.snap b/martin-mbtiles/tests/snapshots/mbtiles__databases@hash__v2.snap new file mode 100644 index 000000000..d794b6e95 --- /dev/null +++ b/martin-mbtiles/tests/snapshots/mbtiles__databases@hash__v2.snap @@ -0,0 +1,53 @@ +--- +source: martin-mbtiles/tests/mbtiles.rs +expression: actual_value +--- +[[]] +type = 'table' +tbl_name = 'metadata' +sql = ''' +CREATE TABLE metadata ( + name text NOT NULL PRIMARY KEY, + value text)''' +values = [ + '( "agg_tiles_hash", "5C90855D70120501451BDD08CA71341A" )', + '( "md-edit", "value - v2" )', + '( "md-new", "value - new" )', + '( "md-same", "value - same" )', +] + +[[]] +type = 'table' +tbl_name = 'tiles_with_hash' +sql = ''' +CREATE TABLE tiles_with_hash ( + zoom_level integer NOT NULL, + tile_column integer NOT NULL, + tile_row integer NOT NULL, + tile_data blob, + tile_hash text, + PRIMARY KEY(zoom_level, tile_column, tile_row))''' +values = [ + '( 5, 0, 0, blob(same), "51037A4A37730F52C8732586D3AAA316" )', + '( 5, 1, 1, blob(edit-v2), "FF76830FF90D79BB335884F256031731" )', + '( 5, 3, 7, blob(new), "22AF645D1859CB5CA6DA0C484F1F37EA" )', + '( 5, 3, 8, blob(new), "22AF645D1859CB5CA6DA0C484F1F37EA" )', + '( 6, 0, 3, blob(same), "51037A4A37730F52C8732586D3AAA316" )', + '( 6, 0, 5, blob(1-keep-1-rm), "535A5575B48444EDEB926815AB26EC9B" )', + '( 6, 1, 4, blob(edit-v2a), "03132BFACDB00CC63D6B7DD98D974DD5" )', +] + +[[]] +type = 'index' +tbl_name = 'metadata' + +[[]] +type = 'index' +tbl_name = 'tiles_with_hash' + +[[]] +type = 'view' +tbl_name = 'tiles' +sql = ''' +CREATE VIEW tiles AS + SELECT zoom_level, tile_column, tile_row, tile_data FROM tiles_with_hash''' diff --git a/martin-mbtiles/tests/snapshots/mbtiles__databases@norm__dif.snap b/martin-mbtiles/tests/snapshots/mbtiles__databases@norm__dif.snap new file mode 100644 index 000000000..e2fe35f9d --- /dev/null +++ b/martin-mbtiles/tests/snapshots/mbtiles__databases@norm__dif.snap @@ -0,0 +1,89 @@ +--- +source: martin-mbtiles/tests/mbtiles.rs +expression: actual_value +--- +[[]] +type = 'table' +tbl_name = 'images' +sql = ''' +CREATE TABLE images ( + tile_id text NOT NULL PRIMARY KEY, + tile_data blob)''' +values = [ + '( "", NULL )', + '( "03132BFACDB00CC63D6B7DD98D974DD5", blob(edit-v2a) )', + '( "22AF645D1859CB5CA6DA0C484F1F37EA", blob(new) )', + '( "FF76830FF90D79BB335884F256031731", blob(edit-v2) )', +] + +[[]] +type = 'table' +tbl_name = 'map' +sql = ''' +CREATE TABLE map ( + zoom_level integer NOT NULL, + tile_column integer NOT NULL, + tile_row integer NOT NULL, + tile_id text, + PRIMARY KEY(zoom_level, tile_column, tile_row))''' +values = [ + '( 5, 1, 1, "FF76830FF90D79BB335884F256031731" )', + '( 5, 2, 2, "" )', + '( 5, 3, 7, "22AF645D1859CB5CA6DA0C484F1F37EA" )', + '( 5, 3, 8, "22AF645D1859CB5CA6DA0C484F1F37EA" )', + '( 6, 1, 4, "03132BFACDB00CC63D6B7DD98D974DD5" )', + '( 6, 2, 6, "" )', +] + +[[]] +type = 'table' +tbl_name = 'metadata' +sql = ''' +CREATE TABLE metadata ( + name text NOT NULL PRIMARY KEY, + value text)''' +values = [ + '( "agg_tiles_hash", "AB9EE21538C1D28BB357ABB3A45BD6BD" )', + '( "agg_tiles_hash_after_apply", "5C90855D70120501451BDD08CA71341A" )', + '( "md-edit", "value - v2" )', + '( "md-new", "value - new" )', + '( "md-remove", NULL )', +] + +[[]] +type = 'index' +tbl_name = 'images' + +[[]] +type = 'index' +tbl_name = 'map' + +[[]] +type = 'index' +tbl_name = 'metadata' + +[[]] +type = 'view' +tbl_name = 'tiles' +sql = ''' +CREATE VIEW tiles AS + SELECT map.zoom_level AS zoom_level, + map.tile_column AS tile_column, + map.tile_row AS tile_row, + images.tile_data AS tile_data + FROM map + JOIN images ON images.tile_id = map.tile_id''' + +[[]] +type = 'view' +tbl_name = 'tiles_with_hash' +sql = ''' +CREATE VIEW tiles_with_hash AS + SELECT + map.zoom_level AS zoom_level, + map.tile_column AS tile_column, + map.tile_row AS tile_row, + images.tile_data AS tile_data, + images.tile_id AS tile_hash + FROM map + JOIN images ON images.tile_id = map.tile_id''' diff --git a/martin-mbtiles/tests/snapshots/mbtiles__databases@norm__v1-no-hash.snap b/martin-mbtiles/tests/snapshots/mbtiles__databases@norm__v1-no-hash.snap new file mode 100644 index 000000000..3586bffb1 --- /dev/null +++ b/martin-mbtiles/tests/snapshots/mbtiles__databases@norm__v1-no-hash.snap @@ -0,0 +1,88 @@ +--- +source: martin-mbtiles/tests/mbtiles.rs +expression: actual_value +--- +[[]] +type = 'table' +tbl_name = 'images' +sql = ''' +CREATE TABLE images ( + tile_id text NOT NULL PRIMARY KEY, + tile_data blob)''' +values = [ + '( "0F6969D7052DA9261E31DDB6E88C136E", blob(remove) )', + '( "51037A4A37730F52C8732586D3AAA316", blob(same) )', + '( "535A5575B48444EDEB926815AB26EC9B", blob(1-keep-1-rm) )', + '( "EFE0AE5FD114DE99855BC2838BE97E1D", blob(edit-v1) )', +] + +[[]] +type = 'table' +tbl_name = 'map' +sql = ''' +CREATE TABLE map ( + zoom_level integer NOT NULL, + tile_column integer NOT NULL, + tile_row integer NOT NULL, + tile_id text, + PRIMARY KEY(zoom_level, tile_column, tile_row))''' +values = [ + '( 5, 0, 0, "51037A4A37730F52C8732586D3AAA316" )', + '( 5, 1, 1, "EFE0AE5FD114DE99855BC2838BE97E1D" )', + '( 5, 2, 2, "0F6969D7052DA9261E31DDB6E88C136E" )', + '( 6, 0, 3, "51037A4A37730F52C8732586D3AAA316" )', + '( 6, 0, 5, "535A5575B48444EDEB926815AB26EC9B" )', + '( 6, 1, 4, "EFE0AE5FD114DE99855BC2838BE97E1D" )', + '( 6, 2, 6, "535A5575B48444EDEB926815AB26EC9B" )', +] + +[[]] +type = 'table' +tbl_name = 'metadata' +sql = ''' +CREATE TABLE metadata ( + name text NOT NULL PRIMARY KEY, + value text)''' +values = [ + '( "md-edit", "value - v1" )', + '( "md-remove", "value - remove" )', + '( "md-same", "value - same" )', +] + +[[]] +type = 'index' +tbl_name = 'images' + +[[]] +type = 'index' +tbl_name = 'map' + +[[]] +type = 'index' +tbl_name = 'metadata' + +[[]] +type = 'view' +tbl_name = 'tiles' +sql = ''' +CREATE VIEW tiles AS + SELECT map.zoom_level AS zoom_level, + map.tile_column AS tile_column, + map.tile_row AS tile_row, + images.tile_data AS tile_data + FROM map + JOIN images ON images.tile_id = map.tile_id''' + +[[]] +type = 'view' +tbl_name = 'tiles_with_hash' +sql = ''' +CREATE VIEW tiles_with_hash AS + SELECT + map.zoom_level AS zoom_level, + map.tile_column AS tile_column, + map.tile_row AS tile_row, + images.tile_data AS tile_data, + images.tile_id AS tile_hash + FROM map + JOIN images ON images.tile_id = map.tile_id''' diff --git a/martin-mbtiles/tests/snapshots/mbtiles__databases@norm__v1.snap b/martin-mbtiles/tests/snapshots/mbtiles__databases@norm__v1.snap new file mode 100644 index 000000000..ea71fb5f8 --- /dev/null +++ b/martin-mbtiles/tests/snapshots/mbtiles__databases@norm__v1.snap @@ -0,0 +1,89 @@ +--- +source: martin-mbtiles/tests/mbtiles.rs +expression: actual_value +--- +[[]] +type = 'table' +tbl_name = 'images' +sql = ''' +CREATE TABLE images ( + tile_id text NOT NULL PRIMARY KEY, + tile_data blob)''' +values = [ + '( "0F6969D7052DA9261E31DDB6E88C136E", blob(remove) )', + '( "51037A4A37730F52C8732586D3AAA316", blob(same) )', + '( "535A5575B48444EDEB926815AB26EC9B", blob(1-keep-1-rm) )', + '( "EFE0AE5FD114DE99855BC2838BE97E1D", blob(edit-v1) )', +] + +[[]] +type = 'table' +tbl_name = 'map' +sql = ''' +CREATE TABLE map ( + zoom_level integer NOT NULL, + tile_column integer NOT NULL, + tile_row integer NOT NULL, + tile_id text, + PRIMARY KEY(zoom_level, tile_column, tile_row))''' +values = [ + '( 5, 0, 0, "51037A4A37730F52C8732586D3AAA316" )', + '( 5, 1, 1, "EFE0AE5FD114DE99855BC2838BE97E1D" )', + '( 5, 2, 2, "0F6969D7052DA9261E31DDB6E88C136E" )', + '( 6, 0, 3, "51037A4A37730F52C8732586D3AAA316" )', + '( 6, 0, 5, "535A5575B48444EDEB926815AB26EC9B" )', + '( 6, 1, 4, "EFE0AE5FD114DE99855BC2838BE97E1D" )', + '( 6, 2, 6, "535A5575B48444EDEB926815AB26EC9B" )', +] + +[[]] +type = 'table' +tbl_name = 'metadata' +sql = ''' +CREATE TABLE metadata ( + name text NOT NULL PRIMARY KEY, + value text)''' +values = [ + '( "agg_tiles_hash", "0063DADF9C78A376418DB0D2B00A5F80" )', + '( "md-edit", "value - v1" )', + '( "md-remove", "value - remove" )', + '( "md-same", "value - same" )', +] + +[[]] +type = 'index' +tbl_name = 'images' + +[[]] +type = 'index' +tbl_name = 'map' + +[[]] +type = 'index' +tbl_name = 'metadata' + +[[]] +type = 'view' +tbl_name = 'tiles' +sql = ''' +CREATE VIEW tiles AS + SELECT map.zoom_level AS zoom_level, + map.tile_column AS tile_column, + map.tile_row AS tile_row, + images.tile_data AS tile_data + FROM map + JOIN images ON images.tile_id = map.tile_id''' + +[[]] +type = 'view' +tbl_name = 'tiles_with_hash' +sql = ''' +CREATE VIEW tiles_with_hash AS + SELECT + map.zoom_level AS zoom_level, + map.tile_column AS tile_column, + map.tile_row AS tile_row, + images.tile_data AS tile_data, + images.tile_id AS tile_hash + FROM map + JOIN images ON images.tile_id = map.tile_id''' diff --git a/martin-mbtiles/tests/snapshots/mbtiles__databases@norm__v2.snap b/martin-mbtiles/tests/snapshots/mbtiles__databases@norm__v2.snap new file mode 100644 index 000000000..46b88cdcb --- /dev/null +++ b/martin-mbtiles/tests/snapshots/mbtiles__databases@norm__v2.snap @@ -0,0 +1,90 @@ +--- +source: martin-mbtiles/tests/mbtiles.rs +expression: actual_value +--- +[[]] +type = 'table' +tbl_name = 'images' +sql = ''' +CREATE TABLE images ( + tile_id text NOT NULL PRIMARY KEY, + tile_data blob)''' +values = [ + '( "03132BFACDB00CC63D6B7DD98D974DD5", blob(edit-v2a) )', + '( "22AF645D1859CB5CA6DA0C484F1F37EA", blob(new) )', + '( "51037A4A37730F52C8732586D3AAA316", blob(same) )', + '( "535A5575B48444EDEB926815AB26EC9B", blob(1-keep-1-rm) )', + '( "FF76830FF90D79BB335884F256031731", blob(edit-v2) )', +] + +[[]] +type = 'table' +tbl_name = 'map' +sql = ''' +CREATE TABLE map ( + zoom_level integer NOT NULL, + tile_column integer NOT NULL, + tile_row integer NOT NULL, + tile_id text, + PRIMARY KEY(zoom_level, tile_column, tile_row))''' +values = [ + '( 5, 0, 0, "51037A4A37730F52C8732586D3AAA316" )', + '( 5, 1, 1, "FF76830FF90D79BB335884F256031731" )', + '( 5, 3, 7, "22AF645D1859CB5CA6DA0C484F1F37EA" )', + '( 5, 3, 8, "22AF645D1859CB5CA6DA0C484F1F37EA" )', + '( 6, 0, 3, "51037A4A37730F52C8732586D3AAA316" )', + '( 6, 0, 5, "535A5575B48444EDEB926815AB26EC9B" )', + '( 6, 1, 4, "03132BFACDB00CC63D6B7DD98D974DD5" )', +] + +[[]] +type = 'table' +tbl_name = 'metadata' +sql = ''' +CREATE TABLE metadata ( + name text NOT NULL PRIMARY KEY, + value text)''' +values = [ + '( "agg_tiles_hash", "5C90855D70120501451BDD08CA71341A" )', + '( "md-edit", "value - v2" )', + '( "md-new", "value - new" )', + '( "md-same", "value - same" )', +] + +[[]] +type = 'index' +tbl_name = 'images' + +[[]] +type = 'index' +tbl_name = 'map' + +[[]] +type = 'index' +tbl_name = 'metadata' + +[[]] +type = 'view' +tbl_name = 'tiles' +sql = ''' +CREATE VIEW tiles AS + SELECT map.zoom_level AS zoom_level, + map.tile_column AS tile_column, + map.tile_row AS tile_row, + images.tile_data AS tile_data + FROM map + JOIN images ON images.tile_id = map.tile_id''' + +[[]] +type = 'view' +tbl_name = 'tiles_with_hash' +sql = ''' +CREATE VIEW tiles_with_hash AS + SELECT + map.zoom_level AS zoom_level, + map.tile_column AS tile_column, + map.tile_row AS tile_row, + images.tile_data AS tile_data, + images.tile_id AS tile_hash + FROM map + JOIN images ON images.tile_id = map.tile_id''' diff --git a/martin/src/bin/main.rs b/martin/src/bin/main.rs index c97ac21f4..6501c098a 100644 --- a/martin/src/bin/main.rs +++ b/martin/src/bin/main.rs @@ -58,7 +58,7 @@ async fn start(args: Args) -> Result { #[actix_web::main] async fn main() { - let env = env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "martin=info"); + let env = env_logger::Env::default().default_filter_or("martin=info"); env_logger::Builder::from_env(env).init(); start(Args::parse()) diff --git a/martin/src/pg/table_source.rs b/martin/src/pg/table_source.rs index 9fe61a913..d26adad85 100644 --- a/martin/src/pg/table_source.rs +++ b/martin/src/pg/table_source.rs @@ -261,8 +261,6 @@ pub fn calc_srid( Some(default_srid) } (0, 0, None) => { - // TODO: cleanup - // println!("{:#?}", std::backtrace::Backtrace::force_capture()); let info = "To use this table source, set default or specify this table SRID in the config file, or set the default SRID with --default-srid=..."; warn!("Table {table_id} has SRID=0, skipping. {info}"); None diff --git a/tests/expected/mbtiles/copy_diff.txt b/tests/expected/mbtiles/copy_diff.txt index 4fc7c848d..4a0e287bc 100644 --- a/tests/expected/mbtiles/copy_diff.txt +++ b/tests/expected/mbtiles/copy_diff.txt @@ -1 +1,2 @@ -[INFO ] Creating new metadata value agg_tiles_hash = C7E2E5A9BA04693994DB1F57D1DF5646 in tests/temp/world_cities_diff.mbtiles +[INFO ] Comparing ./tests/fixtures/mbtiles/world_cities.mbtiles (flat) and ./tests/fixtures/mbtiles/world_cities_modified.mbtiles (flat) into a new file tests/temp/world_cities_diff.mbtiles (flat) +[INFO ] Adding a new metadata value agg_tiles_hash = C7E2E5A9BA04693994DB1F57D1DF5646 in tests/temp/world_cities_diff.mbtiles diff --git a/tests/expected/mbtiles/copy_diff2.txt b/tests/expected/mbtiles/copy_diff2.txt index cddb2637d..e03d7f71a 100644 --- a/tests/expected/mbtiles/copy_diff2.txt +++ b/tests/expected/mbtiles/copy_diff2.txt @@ -1 +1,2 @@ -[INFO ] Creating new metadata value agg_tiles_hash = D41D8CD98F00B204E9800998ECF8427E in tests/temp/world_cities_diff_modified.mbtiles +[INFO ] Comparing ./tests/fixtures/mbtiles/world_cities_modified.mbtiles (flat) and tests/temp/world_cities_copy.mbtiles (flat) into a new file tests/temp/world_cities_diff_modified.mbtiles (flat) +[INFO ] Adding a new metadata value agg_tiles_hash = D41D8CD98F00B204E9800998ECF8427E in tests/temp/world_cities_diff_modified.mbtiles diff --git a/tests/fixtures/mbtiles/world_cities.mbties b/tests/fixtures/mbtiles/world_cities.mbties new file mode 100644 index 000000000..e69de29bb diff --git a/tests/pg_server_test.rs b/tests/pg_server_test.rs index 1d65869c3..7f4037a15 100644 --- a/tests/pg_server_test.rs +++ b/tests/pg_server_test.rs @@ -877,7 +877,6 @@ postgres: let req = test_get("/function_zxy_query_test/0/0/0"); let response = call_service(&app, req).await; - println!("response.status = {:?}", response.status()); assert!(response.status().is_server_error()); let req = test_get("/function_zxy_query_test/0/0/0?token=martin"); @@ -990,7 +989,9 @@ tables: ) .await; - let OneOrMany::One(cfg) = cfg.postgres.unwrap() else { panic!() }; + let OneOrMany::One(cfg) = cfg.postgres.unwrap() else { + panic!() + }; for (name, _) in cfg.tables.unwrap_or_default() { let req = test_get(format!("/{name}/0/0/0").as_str()); let response = call_service(&app, req).await; From 281799021dfbb8b1a8460d681608d7e31a67f287 Mon Sep 17 00:00:00 2001 From: Ruben Poppe Date: Wed, 11 Oct 2023 03:28:07 +0200 Subject: [PATCH 032/108] Update Homebrew for 0.9.0 (#932) - Updated version to 0.9.0, removed openssl dependency - Added architecture checks --- HomebrewFormula/martin.rb | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/HomebrewFormula/martin.rb b/HomebrewFormula/martin.rb index 006ba31a3..7c51151d7 100644 --- a/HomebrewFormula/martin.rb +++ b/HomebrewFormula/martin.rb @@ -1,18 +1,20 @@ class Martin < Formula - current_version="0.8.7" + current_version="0.9.0" desc "Blazing fast and lightweight tile server with PostGIS, MBTiles, and PMTiles support" homepage "https://github.com/maplibre/martin" - url "https://github.com/maplibre/martin/releases/download/v#{current_version}/martin-Darwin-x86_64.tar.gz" - # This is the sha256 checksum of the martin-Darwin-x86_64.tar.gz file - # I am not certain if arch64 should have a different sha256 somewhere - sha256 "92f660b1bef3a54dc84e4794a5ba02a8817c25f21ce7000783749bbae9e50de1" + on_arm do + sha256 "d1a64d4707e3f1fdb41b3e445c462465e6150d3b30f7520af262548184a4d08b" + url "https://github.com/maplibre/martin/releases/download/v#{current_version}/martin-Darwin-aarch64.tar.gz" + end + on_intel do + sha256 "ab581373a9fe699ba8e6640b587669391c6d5901ce816c3acb154f8410775068" + url "https://github.com/maplibre/martin/releases/download/v#{current_version}/martin-Darwin-x86_64.tar.gz" + end + version "#{current_version}" - # FIXME: remove this for the 0.9 version - depends_on "openssl@3" - def install bin.install "martin" end From 58d691dd10874d197bd127052be314df1c261406 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Wed, 11 Oct 2023 16:31:30 -0400 Subject: [PATCH 033/108] Use chmod +x in packaging, move homebrew to 9.1 --- .github/workflows/ci.yml | 6 +++--- Cargo.lock | 17 ++++++++--------- HomebrewFormula/martin.rb | 10 ++++++---- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a67acba26..aca897934 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -520,7 +520,6 @@ jobs: - target: x86_64-pc-windows-msvc os: windows-latest name: martin-Windows-x86_64.zip - ext: '.exe' - target: x86_64-unknown-linux-gnu os: ubuntu-latest name: martin-Linux-x86_64.tar.gz @@ -554,14 +553,15 @@ jobs: run: | cd target/ if [[ "${{ runner.os }}" == "Windows" ]]; then - 7z a ../${{ matrix.name }} martin${{ matrix.ext }} mbtiles${{ matrix.ext }} + 7z a ../${{ matrix.name }} martin.exe mbtiles.exe elif [[ "${{ matrix.target }}" == "debian-x86_64" ]]; then mv debian-x86_64.deb ../${{ matrix.name }} else if [[ "${{ matrix.cross }}" == "true" ]]; then mv ${{ matrix.target }}/* . fi - tar czvf ../${{ matrix.name }} martin${{ matrix.ext }} mbtiles${{ matrix.ext }} + chmod +x martin mbtiles + tar czvf ../${{ matrix.name }} martin mbtiles fi # TODO: why is this needed and where should the result go? # - name: Generate SHA-256 (MacOS) diff --git a/Cargo.lock b/Cargo.lock index 06a2b0e08..12047afcc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1580,9 +1580,9 @@ checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "jobserver" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" +checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" dependencies = [ "libc", ] @@ -2379,7 +2379,7 @@ dependencies = [ "aho-corasick", "memchr", "regex-automata", - "regex-syntax 0.8.0", + "regex-syntax 0.8.1", ] [[package]] @@ -2390,7 +2390,7 @@ checksum = "465c6fc0621e4abc4187a2bda0937bfd4f722c2730b29562e19689ea796c9a4b" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.0", + "regex-syntax 0.8.1", ] [[package]] @@ -2401,9 +2401,9 @@ checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" [[package]] name = "regex-syntax" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3cbb081b9784b07cceb8824c8583f86db4814d172ab043f3c23f7dc600bf83d" +checksum = "56d84fdd47036b038fc80dd333d10b6aab10d5d31f4a366e20014def75328d33" [[package]] name = "relative-path" @@ -4059,11 +4059,10 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "2.0.8+zstd.1.5.5" +version = "2.0.9+zstd.1.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5556e6ee25d32df2586c098bbfa278803692a20d0ab9565e049480d52707ec8c" +checksum = "9e16efa8a874a0481a574084d34cc26fdb3b99627480f785888deb6386506656" dependencies = [ "cc", - "libc", "pkg-config", ] diff --git a/HomebrewFormula/martin.rb b/HomebrewFormula/martin.rb index 7c51151d7..3e08cacd7 100644 --- a/HomebrewFormula/martin.rb +++ b/HomebrewFormula/martin.rb @@ -1,15 +1,15 @@ class Martin < Formula - current_version="0.9.0" + current_version="0.9.1" - desc "Blazing fast and lightweight tile server with PostGIS, MBTiles, and PMTiles support" + desc "Blazing fast and lightweight tile server with PostGIS, MBTiles, and PMTiles support, plus an mbtiles tool" homepage "https://github.com/maplibre/martin" on_arm do - sha256 "d1a64d4707e3f1fdb41b3e445c462465e6150d3b30f7520af262548184a4d08b" + sha256 "00828eb3490664eba767323da98d2847238b65b7bea1e235267e43a67277d8e5" url "https://github.com/maplibre/martin/releases/download/v#{current_version}/martin-Darwin-aarch64.tar.gz" end on_intel do - sha256 "ab581373a9fe699ba8e6640b587669391c6d5901ce816c3acb154f8410775068" + sha256 "75b52bd89ba397267080e938dd261f57c1eabdaa1d27ac13bf4904031672a6e9" url "https://github.com/maplibre/martin/releases/download/v#{current_version}/martin-Darwin-x86_64.tar.gz" end @@ -17,6 +17,7 @@ class Martin < Formula def install bin.install "martin" + bin.install "mbtiles" end def caveats; <<~EOS @@ -28,5 +29,6 @@ def caveats; <<~EOS test do `#{bin}/martin --version` + `#{bin}/mbtiles --version` end end From cb7c49b7064b1fd7f978b209039dae26e4032178 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Wed, 11 Oct 2023 21:56:37 -0400 Subject: [PATCH 034/108] Move homebrew (#933) The new HomeBrew is now in https://github.com/maplibre/homebrew-martin cc: @rubenpoppe --- HomebrewFormula/martin.rb | 34 ---------------------------------- README.md | 6 +++--- 2 files changed, 3 insertions(+), 37 deletions(-) delete mode 100644 HomebrewFormula/martin.rb diff --git a/HomebrewFormula/martin.rb b/HomebrewFormula/martin.rb deleted file mode 100644 index 3e08cacd7..000000000 --- a/HomebrewFormula/martin.rb +++ /dev/null @@ -1,34 +0,0 @@ -class Martin < Formula - current_version="0.9.1" - - desc "Blazing fast and lightweight tile server with PostGIS, MBTiles, and PMTiles support, plus an mbtiles tool" - homepage "https://github.com/maplibre/martin" - - on_arm do - sha256 "00828eb3490664eba767323da98d2847238b65b7bea1e235267e43a67277d8e5" - url "https://github.com/maplibre/martin/releases/download/v#{current_version}/martin-Darwin-aarch64.tar.gz" - end - on_intel do - sha256 "75b52bd89ba397267080e938dd261f57c1eabdaa1d27ac13bf4904031672a6e9" - url "https://github.com/maplibre/martin/releases/download/v#{current_version}/martin-Darwin-x86_64.tar.gz" - end - - version "#{current_version}" - - def install - bin.install "martin" - bin.install "mbtiles" - end - - def caveats; <<~EOS - Martin requires a database connection string. - It can be passed as a command-line argument or as a DATABASE_URL environment variable. - martin postgres://postgres@localhost/db - EOS - end - - test do - `#{bin}/martin --version` - `#{bin}/mbtiles --version` - end -end diff --git a/README.md b/README.md index b66c18be9..50dd46fe9 100755 --- a/README.md +++ b/README.md @@ -37,11 +37,11 @@ You can download martin from [GitHub releases page](https://github.com/maplibre/ [rl-macos-a64]: https://github.com/maplibre/martin/releases/latest/download/martin-Darwin-aarch64.tar.gz [rl-win64-zip]: https://github.com/maplibre/martin/releases/latest/download/martin-Windows-x86_64.zip -If you are using macOS and [Homebrew](https://brew.sh/) you can install martin using Homebrew tap. +If you are using macOS and [Homebrew](https://brew.sh/) you can install `martin` and `mbtiles` using Homebrew tap. ```shell -brew tap maplibre/martin https://github.com/maplibre/martin.git -brew install maplibre/martin/martin +brew tap maplibre/martin +brew install martin ``` ## Running Martin Service From dee34313aed7e35f0878b01602607769669b7950 Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 12 Oct 2023 10:44:27 +0800 Subject: [PATCH 035/108] Updat doc about homebrew installation (#934) --- docs/src/installation.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/installation.md b/docs/src/installation.md index 908d98925..7f0b9a364 100644 --- a/docs/src/installation.md +++ b/docs/src/installation.md @@ -30,8 +30,8 @@ martin --help If you are using macOS and [Homebrew](https://brew.sh/) you can install martin using Homebrew tap. ```shell -brew tap maplibre/martin https://github.com/maplibre/martin.git -brew install maplibre/martin/martin +brew tap maplibre/martin +brew install martin ``` ## Docker From f40090ab09af80a73a2c188fa2925f1507dfa7ee Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Wed, 11 Oct 2023 22:53:30 -0400 Subject: [PATCH 036/108] v0.9.3 cleanups --- .github/workflows/bench.yml | 10 ++++------ .github/workflows/ci.yml | 6 ------ .github/workflows/grcov.yml | 2 -- CHANGELOG.md | 3 +++ Cargo.lock | 2 +- martin/Cargo.toml | 5 ++--- 6 files changed, 10 insertions(+), 18 deletions(-) diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 5cbec58c4..1fad55133 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -1,20 +1,18 @@ name: Benchmark on: -# pull_request: -# types: [ opened, synchronize, reopened ] +# push: +# branches: [ main ] # paths-ignore: # - '**.md' # - 'demo/**' # - 'docs/**' -# - 'homebrew-formula/**' -# push: -# branches: [ main ] +# pull_request: +# types: [ opened, synchronize, reopened ] # paths-ignore: # - '**.md' # - 'demo/**' # - 'docs/**' -# - 'homebrew-formula/**' workflow_dispatch: jobs: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aca897934..44750163f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,18 +3,12 @@ name: CI on: push: branches: [ main ] - paths-ignore: - - '**.md' - - 'demo/**' - - 'docs/**' - - 'HomebrewFormula/**' pull_request: branches: [ main ] paths-ignore: - '**.md' - 'demo/**' - 'docs/**' - - 'HomebrewFormula/**' release: types: [ published ] workflow_dispatch: diff --git a/.github/workflows/grcov.yml b/.github/workflows/grcov.yml index 4d7f22b44..a79833858 100644 --- a/.github/workflows/grcov.yml +++ b/.github/workflows/grcov.yml @@ -7,14 +7,12 @@ on: - '**.md' - 'demo/**' - 'docs/**' - - 'HomebrewFormula/**' pull_request: branches: [ main ] paths-ignore: - '**.md' - 'demo/**' - 'docs/**' - - 'HomebrewFormula/**' workflow_dispatch: jobs: diff --git a/CHANGELOG.md b/CHANGELOG.md index ea88cc71b..bfa6652ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ +# ATTENTION +This file is currently not maintained. See [release](https://github.com/maplibre/martin/releases) instead. + ## [Unreleased] - ReleaseDate ### ⚠ BREAKING CHANGES diff --git a/Cargo.lock b/Cargo.lock index 12047afcc..0eb032c27 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1726,7 +1726,7 @@ checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "martin" -version = "0.9.1" +version = "0.9.3" dependencies = [ "actix", "actix-cors", diff --git a/martin/Cargo.toml b/martin/Cargo.toml index 727317cea..750dca484 100644 --- a/martin/Cargo.toml +++ b/martin/Cargo.toml @@ -1,8 +1,7 @@ [package] name = "martin" -# Make sure to update /home/nyurik/dev/rust/martin/HomebrewFormula/martin.rb version -# Once the release is published with the hash -version = "0.9.1" +# Once the release is published with the hash, update https://github.com/maplibre/homebrew-martin +version = "0.9.3" authors = ["Stepan Kuzmin ", "Yuri Astrakhan ", "MapLibre contributors"] description = "Blazing fast and lightweight tile server with PostGIS, MBTiles, and PMTiles support" keywords = ["maps", "tiles", "mbtiles", "pmtiles", "postgis"] From 9e7422645fa5e840a5206b038cb8074569128523 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Thu, 12 Oct 2023 00:37:03 -0400 Subject: [PATCH 037/108] Auto-update homebrew on release (#935) --- .github/templates/homebrew.martin.rb.j2 | 52 +++++++ .github/workflows/ci.yml | 179 ++++++++++++++++-------- 2 files changed, 171 insertions(+), 60 deletions(-) create mode 100644 .github/templates/homebrew.martin.rb.j2 diff --git a/.github/templates/homebrew.martin.rb.j2 b/.github/templates/homebrew.martin.rb.j2 new file mode 100644 index 000000000..fcf4c6fe1 --- /dev/null +++ b/.github/templates/homebrew.martin.rb.j2 @@ -0,0 +1,52 @@ +# +# ATTENTION: This is an autogenerated file. See original at +# https://github.com/maplibre/martin/blob/main/.github/templates/homebrew.martin.rb.j2 +# + +class Martin < Formula + current_version="{{ version }}" + + desc "Blazing fast and lightweight tile server with PostGIS, MBTiles, and PMTiles support, plus an mbtiles tool" + homepage "https://github.com/maplibre/martin" + + on_macos do + on_arm do + sha256 "{{ macos_arm_sha256 }}}" + url "https://github.com/maplibre/martin/releases/download/v#{current_version}/martin-Darwin-aarch64.tar.gz" + end + on_intel do + sha256 "{{ macos_intel_sha256 }}" + url "https://github.com/maplibre/martin/releases/download/v#{current_version}/martin-Darwin-x86_64.tar.gz" + end + end + + on_linux do + on_arm do + sha256 "{{ linux_arm_sha256 }}" + url "https://github.com/maplibre/martin/releases/download/v#{current_version}/martin-Linux-aarch64-musl.tar.gz" + end + on_intel do + sha256 "{{ linux_intel_sha256 }}" + url "https://github.com/maplibre/martin/releases/download/v#{current_version}/martin-Linux-x86_64-musl.tar.gz" + end + end + + version "#{current_version}" + + def install + bin.install "martin" + bin.install "mbtiles" + end + + def caveats; <<~EOS + Martin requires a database connection string. + It can be passed as a command-line argument or as a DATABASE_URL environment variable. + martin postgres://postgres@localhost/db + EOS + end + + test do + `#{bin}/martin --version` + `#{bin}/mbtiles --version` + end +end diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 44750163f..be9e04595 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,7 +49,7 @@ jobs: postgis/postgis:16-3.4 -c "exec docker-entrypoint.sh postgres -c ssl=on -c ssl_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem -c ssl_key_file=/etc/ssl/private/ssl-cert-snakeoil.key" steps: - - name: Checkout + - name: Checkout sources uses: actions/checkout@v4 - name: Rust Versions run: rustc --version && cargo --version @@ -253,7 +253,7 @@ jobs: - target: x86_64-unknown-linux-gnu os: ubuntu-latest steps: - - name: Checkout + - name: Checkout sources uses: actions/checkout@v4 - name: Rust Versions run: rustc --version && cargo --version @@ -493,84 +493,143 @@ jobs: retention-days: 5 package: - name: Package ${{ matrix.target }} - runs-on: ${{ matrix.os }} + name: Package + runs-on: ubuntu-latest needs: [ lint-debug-test, docker-build-test, test-multi-os, test-with-svc ] - strategy: - fail-fast: true - matrix: - include: - - target: aarch64-apple-darwin - os: ubuntu-latest - name: martin-Darwin-aarch64.tar.gz - sha: 'true' - - target: debian-x86_64 - os: ubuntu-latest - name: martin-Debian-x86_64.deb - - target: x86_64-apple-darwin - os: macOS-latest - name: martin-Darwin-x86_64.tar.gz - sha: 'true' - - target: x86_64-pc-windows-msvc - os: windows-latest - name: martin-Windows-x86_64.zip - - target: x86_64-unknown-linux-gnu - os: ubuntu-latest - name: martin-Linux-x86_64.tar.gz - # - # From the cross-build - # - - target: aarch64-unknown-linux-musl - os: ubuntu-latest - cross: 'true' - name: martin-Linux-aarch64-musl.tar.gz - - target: x86_64-unknown-linux-musl - os: ubuntu-latest - cross: 'true' - name: martin-Linux-x86_64-musl.tar.gz steps: - name: Checkout sources uses: actions/checkout@v4 - - name: Download build artifact build-${{ matrix.target }} - if: matrix.cross != 'true' + - name: Download build artifact build-aarch64-apple-darwin uses: actions/download-artifact@v3 with: - name: build-${{ matrix.target }} - path: target/ - - name: Download cross-build artifact build-${{ matrix.target }} - if: matrix.cross == 'true' + name: build-aarch64-apple-darwin + path: target/aarch64-apple-darwin + - name: Download build artifact build-x86_64-apple-darwin + uses: actions/download-artifact@v3 + with: + name: build-x86_64-apple-darwin + path: target/x86_64-apple-darwin + - name: Download build artifact build-x86_64-unknown-linux-gnu + uses: actions/download-artifact@v3 + with: + name: build-x86_64-unknown-linux-gnu + path: target/x86_64-unknown-linux-gnu + + - name: Download cross-build artifacts uses: actions/download-artifact@v3 with: name: cross-build - path: target/ + path: target/cross + + - name: Download build artifact build-x86_64-pc-windows-msvc + uses: actions/download-artifact@v3 + with: + name: build-x86_64-pc-windows-msvc + path: target/x86_64-pc-windows-msvc + - name: Download build artifact build-debian-x86_64 + uses: actions/download-artifact@v3 + with: + name: build-debian-x86_64 + path: target/debian-x86_64 + - name: Package run: | - cd target/ - if [[ "${{ runner.os }}" == "Windows" ]]; then - 7z a ../${{ matrix.name }} martin.exe mbtiles.exe - elif [[ "${{ matrix.target }}" == "debian-x86_64" ]]; then - mv debian-x86_64.deb ../${{ matrix.name }} - else - if [[ "${{ matrix.cross }}" == "true" ]]; then - mv ${{ matrix.target }}/* . - fi - chmod +x martin mbtiles - tar czvf ../${{ matrix.name }} martin mbtiles - fi - # TODO: why is this needed and where should the result go? - # - name: Generate SHA-256 (MacOS) - # if: matrix.sha == 'true' - # run: shasum -a 256 ${{ matrix.name }} + set -x + + cd target + mkdir files + mv cross/* . + + cd aarch64-apple-darwin + chmod +x martin mbtiles + tar czvf ../files/martin-Darwin-aarch64.tar.gz martin mbtiles + cd .. + + cd x86_64-apple-darwin + chmod +x martin mbtiles + tar czvf ../files/martin-Darwin-x86_64.tar.gz martin mbtiles + cd .. + + cd x86_64-unknown-linux-gnu + chmod +x martin mbtiles + tar czvf ../files/martin-Linux-x86_64.tar.gz martin mbtiles + cd .. + + cd aarch64-unknown-linux-musl + chmod +x martin mbtiles + tar czvf ../files/martin-Linux-aarch64-musl.tar.gz martin mbtiles + cd .. + + cd x86_64-unknown-linux-musl + chmod +x martin mbtiles + tar czvf ../files/martin-Linux-x86_64-musl.tar.gz martin mbtiles + cd .. + + # + # Special case for Windows + # + cd x86_64-pc-windows-msvc + 7z a ../files/martin-Windows-x86_64.zip martin.exe mbtiles.exe + cd .. + + # + # Special case for Debian .deb package + # + cd debian-x86_64 + mv debian-x86_64.deb ../files/martin-Debian-x86_64.deb + cd .. + + mkdir homebrew + cat << EOF > homebrew_config.yaml + version: "${{ github.ref }}" + macos_arm_sha256: "$(shasum -a 256 files/martin-Darwin-aarch64.tar.gz | cut -d' ' -f1)" + macos_intel_sha256: "$(shasum -a 256 files/martin-Darwin-x86_64.tar.gz | cut -d' ' -f1)" + linux_arm_sha256: "$(shasum -a 256 files/martin-Linux-aarch64-musl.tar.gz | cut -d' ' -f1)" + linux_intel_sha256: "$(shasum -a 256 files/martin-Linux-x86_64-musl.tar.gz | cut -d' ' -f1)" + EOF + - name: Publish if: startsWith(github.ref, 'refs/tags/') uses: softprops/action-gh-release@v1 with: draft: true - files: 'martin*' + files: 'target/files/*' body_path: CHANGELOG.md env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Checkout maplibre/homebrew-martin + if: startsWith(github.ref, 'refs/tags/') + uses: actions/checkout@v4 + with: + repository: maplibre/homebrew-martin + path: target/homebrew + + - name: Create Homebrew formula + if: startsWith(github.ref, 'refs/tags/') + uses: cuchi/jinja2-action@v1.2.0 + with: + template: .github/templates/homebrew.martin.rb.j2 + output_file: target/homebrew/martin.rb + data_file: target/homebrew_config.yaml + + - name: Create a PR for maplibre/homebrew-martin + if: startsWith(github.ref, 'refs/tags/') + uses: peter-evans/create-pull-request@v3 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: "Update to ${{ github.ref }}" + title: "Update to ${{ github.ref }}" + body: "Update to ${{ github.ref }}" + branch: "update-to-${{ github.ref }}" + branch-suffix: timestamp + base: "main" + labels: "auto-update" + assignees: "nyurik" + draft: false + delete-branch: true + path: target/homebrew + # This final step is needed to mark the whole workflow as successful # Don't change its name - it is used by the merge protection rules done: From 7114d9837fe0da32f0b8d3df44f7715d3fdaef06 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Thu, 12 Oct 2023 00:44:02 -0400 Subject: [PATCH 038/108] CI: extract refs/tag for homebrew --- .github/workflows/ci.yml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index be9e04595..1e727a221 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -579,14 +579,24 @@ jobs: mv debian-x86_64.deb ../files/martin-Debian-x86_64.deb cd .. + - name: Publish + if: startsWith(github.ref, 'refs/tags/v') + run: | + set -x + + # Extract Github release version only without the "v" prefix + MARTIN_VERSION=$(echo "${{ github.ref }}" | sed -e 's/refs\/tags\/v//') + + cd target mkdir homebrew + cat << EOF > homebrew_config.yaml - version: "${{ github.ref }}" + version: "$MARTIN_VERSION" macos_arm_sha256: "$(shasum -a 256 files/martin-Darwin-aarch64.tar.gz | cut -d' ' -f1)" macos_intel_sha256: "$(shasum -a 256 files/martin-Darwin-x86_64.tar.gz | cut -d' ' -f1)" linux_arm_sha256: "$(shasum -a 256 files/martin-Linux-aarch64-musl.tar.gz | cut -d' ' -f1)" linux_intel_sha256: "$(shasum -a 256 files/martin-Linux-x86_64-musl.tar.gz | cut -d' ' -f1)" - EOF + EOF - name: Publish if: startsWith(github.ref, 'refs/tags/') From 3c24ae226a0f0d3dbaf072416a7ac927160e4da6 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Thu, 12 Oct 2023 01:14:18 -0400 Subject: [PATCH 039/108] version update for pre.1 --- .github/workflows/ci.yml | 2 +- Cargo.lock | 2 +- martin/Cargo.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1e727a221..9e91374aa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -579,7 +579,7 @@ jobs: mv debian-x86_64.deb ../files/martin-Debian-x86_64.deb cd .. - - name: Publish + - name: Create Homebrew config if: startsWith(github.ref, 'refs/tags/v') run: | set -x diff --git a/Cargo.lock b/Cargo.lock index 0eb032c27..b49b641a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1726,7 +1726,7 @@ checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "martin" -version = "0.9.3" +version = "0.9.4-pre.1" dependencies = [ "actix", "actix-cors", diff --git a/martin/Cargo.toml b/martin/Cargo.toml index 750dca484..e3339bda5 100644 --- a/martin/Cargo.toml +++ b/martin/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "martin" # Once the release is published with the hash, update https://github.com/maplibre/homebrew-martin -version = "0.9.3" +version = "0.9.4-pre.1" authors = ["Stepan Kuzmin ", "Yuri Astrakhan ", "MapLibre contributors"] description = "Blazing fast and lightweight tile server with PostGIS, MBTiles, and PMTiles support" keywords = ["maps", "tiles", "mbtiles", "pmtiles", "postgis"] From 4d84371cc5d6287432eedce1d06cf1f851d10551 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Thu, 12 Oct 2023 02:02:03 -0400 Subject: [PATCH 040/108] use token for remote PR creation --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9e91374aa..2ec408a75 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -627,7 +627,7 @@ jobs: if: startsWith(github.ref, 'refs/tags/') uses: peter-evans/create-pull-request@v3 with: - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ secrets.GH_HOMEBREW_MARTIN_TOKEN }} commit-message: "Update to ${{ github.ref }}" title: "Update to ${{ github.ref }}" body: "Update to ${{ github.ref }}" From e204a2fd317e0715c85fe100feae079eecda357d Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Thu, 12 Oct 2023 03:09:02 -0400 Subject: [PATCH 041/108] add PAT to hombrew checkout --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2ec408a75..fba17aaa2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -613,6 +613,7 @@ jobs: uses: actions/checkout@v4 with: repository: maplibre/homebrew-martin + token: ${{ secrets.GH_HOMEBREW_MARTIN_TOKEN }} path: target/homebrew - name: Create Homebrew formula From c926e51ea6a1c3a001ab06c79e0212a35d4c0969 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Thu, 12 Oct 2023 13:57:37 -0400 Subject: [PATCH 042/108] forbid unsafe --- martin/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/martin/src/lib.rs b/martin/src/lib.rs index 9c560b75b..bdc115c76 100644 --- a/martin/src/lib.rs +++ b/martin/src/lib.rs @@ -1,3 +1,4 @@ +#![forbid(unsafe_code)] #![warn(clippy::pedantic)] // Bounds struct derives PartialEq, but not Eq, // so all containing types must also derive PartialEq without Eq From 3db28f9ec3204868cd39c65ffed6a5f5bc9dc8c3 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Thu, 12 Oct 2023 15:24:53 -0400 Subject: [PATCH 043/108] fixing homebrew token --- .github/workflows/ci.yml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fba17aaa2..45595bf58 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -587,8 +587,8 @@ jobs: # Extract Github release version only without the "v" prefix MARTIN_VERSION=$(echo "${{ github.ref }}" | sed -e 's/refs\/tags\/v//') + mkdir -p target/homebrew cd target - mkdir homebrew cat << EOF > homebrew_config.yaml version: "$MARTIN_VERSION" @@ -598,6 +598,13 @@ jobs: linux_intel_sha256: "$(shasum -a 256 files/martin-Linux-x86_64-musl.tar.gz | cut -d' ' -f1)" EOF + - name: Save Homebrew Config + if: startsWith(github.ref, 'refs/tags/') + uses: actions/upload-artifact@v3 + with: + name: homebrew-config + path: target/homebrew_config.yaml + - name: Publish if: startsWith(github.ref, 'refs/tags/') uses: softprops/action-gh-release@v1 @@ -628,6 +635,11 @@ jobs: if: startsWith(github.ref, 'refs/tags/') uses: peter-evans/create-pull-request@v3 with: + # This PTA token is generated with: + # Repository: maplibre/homebrew-martin + # Access Contents: Read-only + # Access Metadata: Read-only + # Access Pull requests: Read and write token: ${{ secrets.GH_HOMEBREW_MARTIN_TOKEN }} commit-message: "Update to ${{ github.ref }}" title: "Update to ${{ github.ref }}" From 846f319bdf5fa2067c25175f3792bd62020e025d Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Thu, 12 Oct 2023 15:27:29 -0400 Subject: [PATCH 044/108] v0.9.3 --- martin/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/martin/Cargo.toml b/martin/Cargo.toml index e3339bda5..750dca484 100644 --- a/martin/Cargo.toml +++ b/martin/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "martin" # Once the release is published with the hash, update https://github.com/maplibre/homebrew-martin -version = "0.9.4-pre.1" +version = "0.9.3" authors = ["Stepan Kuzmin ", "Yuri Astrakhan ", "MapLibre contributors"] description = "Blazing fast and lightweight tile server with PostGIS, MBTiles, and PMTiles support" keywords = ["maps", "tiles", "mbtiles", "pmtiles", "postgis"] From b1b101bd19f16109ca35e7a8aa33769496fa7d1b Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Thu, 12 Oct 2023 15:58:52 -0400 Subject: [PATCH 045/108] fix links --- .github/workflows/ci.yml | 11 ++++++++--- Cargo.lock | 6 +++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 45595bf58..e81deaa44 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -635,9 +635,14 @@ jobs: if: startsWith(github.ref, 'refs/tags/') uses: peter-evans/create-pull-request@v3 with: - # This PTA token is generated with: - # Repository: maplibre/homebrew-martin - # Access Contents: Read-only + # Create a personal access token + # Gen: https://github.com/settings/personal-access-tokens/new + # Set: https://github.com/maplibre/martin/settings/secrets/actions/GH_HOMEBREW_MARTIN_TOKEN + # Docs: https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token + # Name: anything descriptive + # One year long (sadly that's max) + # Repository owner and repo: maplibre/homebrew-martin + # Access Contents: Read and write # Access Metadata: Read-only # Access Pull requests: Read and write token: ${{ secrets.GH_HOMEBREW_MARTIN_TOKEN }} diff --git a/Cargo.lock b/Cargo.lock index b49b641a6..0036ae5d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1726,7 +1726,7 @@ checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "martin" -version = "0.9.4-pre.1" +version = "0.9.3" dependencies = [ "actix", "actix-cors", @@ -2549,9 +2549,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.18" +version = "0.38.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a74ee2d7c2581cd139b42447d7d9389b889bdaad3a73f1ebb16f2a3237bb19c" +checksum = "745ecfa778e66b2b63c88a61cb36e0eea109e803b0b86bf9879fbc77c70e86ed" dependencies = [ "bitflags 2.4.0", "errno", From b2b0e6ff29bca1ef170b3f314c621126e940b5ac Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Thu, 12 Oct 2023 16:10:24 -0400 Subject: [PATCH 046/108] fix spaces --- .github/workflows/ci.yml | 2 +- CHANGELOG.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e81deaa44..dcbc09350 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -595,7 +595,7 @@ jobs: macos_arm_sha256: "$(shasum -a 256 files/martin-Darwin-aarch64.tar.gz | cut -d' ' -f1)" macos_intel_sha256: "$(shasum -a 256 files/martin-Darwin-x86_64.tar.gz | cut -d' ' -f1)" linux_arm_sha256: "$(shasum -a 256 files/martin-Linux-aarch64-musl.tar.gz | cut -d' ' -f1)" - linux_intel_sha256: "$(shasum -a 256 files/martin-Linux-x86_64-musl.tar.gz | cut -d' ' -f1)" + linux_intel_sha256: "$(shasum -a 256 files/martin-Linux-x86_64-musl.tar.gz | cut -d' ' -f1)" EOF - name: Save Homebrew Config diff --git a/CHANGELOG.md b/CHANGELOG.md index bfa6652ef..f75f825e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ # ATTENTION -This file is currently not maintained. See [release](https://github.com/maplibre/martin/releases) instead. +This file is currently not maintained. See [release](https://github.com/maplibre/martin/releases) instead. ## [Unreleased] - ReleaseDate From 10a560674071a8658d5eb58da0018e7ed8ae3a8b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Oct 2023 22:34:16 +0000 Subject: [PATCH 047/108] chore(deps): Bump cuchi/jinja2-action from 1.2.0 to 1.2.1 (#940) Bumps [cuchi/jinja2-action](https://github.com/cuchi/jinja2-action) from 1.2.0 to 1.2.1.
Release notes

Sourced from cuchi/jinja2-action's releases.

1.2.1

  • Perf: Improve Dockerfile layers.
Commits
  • 3fa06ac Merge pull request #14 from funkyfuture/patch-1
  • 6783cff Dockerfile: Reduces layer overhead
  • 55f4190 Merge pull request #11 from cuchi/add-unit-tests
  • fe228f6 chore: fix ci
  • 5a62d10 test: add the unit tests
  • 29b3303 chore: fix enum
  • 841c222 refactor: add enum for inputs
  • 7559ced refactor: render from template in the context class
  • fb642e9 chore: fix Dockerfile
  • 4e1f15a refactor: split context class into its own file
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=cuchi/jinja2-action&package-manager=github_actions&previous-version=1.2.0&new-version=1.2.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dcbc09350..6a4498e78 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -625,7 +625,7 @@ jobs: - name: Create Homebrew formula if: startsWith(github.ref, 'refs/tags/') - uses: cuchi/jinja2-action@v1.2.0 + uses: cuchi/jinja2-action@v1.2.1 with: template: .github/templates/homebrew.martin.rb.j2 output_file: target/homebrew/martin.rb From a42ea5c0ae7751fbbf36997dfa50371064769345 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Oct 2023 18:49:41 -0400 Subject: [PATCH 048/108] chore(deps): Bump peter-evans/create-pull-request from 3 to 5 (#939) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 3 to 5.
Release notes

Sourced from peter-evans/create-pull-request's releases.

Create Pull Request v5.0.0

Behaviour changes

  • The action will no longer leave the local repository checked out on the pull request branch. Instead, it will leave the repository checked out on the branch or commit that it was when the action started.
  • When using add-paths, uncommitted changes will no longer be destroyed. They will be stashed and restored at the end of the action run.

What's new

  • Adds input body-path, the path to a file containing the pull request body.
  • At the end of the action run the local repository is now checked out on the branch or commit that it was when the action started.
  • Any uncommitted tracked or untracked changes are now stashed and restored at the end of the action run. Currently, this can only occur when using the add-paths input, which allows for changes to not be committed. Previously, any uncommitted changes would be destroyed.
  • The proxy implementation has been revised but is not expected to have any change in behaviour. It continues to support the standard environment variables http_proxy, https_proxy and no_proxy.
  • Now sets the git safe.directory configuration for the local repository path. The configuration is removed when the action completes. Fixes issue peter-evans/create-pull-request#1170.
  • Now determines the git directory path using the git rev-parse --git-dir command. This allows users with custom repository configurations to use the action.
  • Improved handling of the team-reviewers input and associated errors.

News

:trophy: create-pull-request won an award for "awesome action" at the Open Source Awards at GitHub Universe. Thank you for your support and for making create-pull-request one of the top used actions. Please give it a :star:, or even buy me a coffee.

What's Changed

Full Changelog: https://github.com/peter-evans/create-pull-request/compare/v4.2.4...v5.0.0

Create Pull Request v4.2.4

⚙️ Patches some recent security vulnerabilities.

What's Changed

New Contributors

Full Changelog: https://github.com/peter-evans/create-pull-request/compare/v4.2.3...v4.2.4

Create Pull Request v4.2.3

What's Changed

Full Changelog: https://github.com/peter-evans/create-pull-request/compare/v4.2.2...v4.2.3

Create Pull Request v4.2.2

What's Changed

... (truncated)

Commits
  • 1534078 fix: specify head repo (#2044)
  • 143be5d build(deps-dev): bump @​typescript-eslint/parser from 5.59.9 to 5.59.11 (#2048)
  • 51e8ca2 build(deps-dev): bump @​types/node from 18.16.16 to 18.16.18 (#2047)
  • 712add8 build(deps-dev): bump @​types/jest from 29.5.1 to 29.5.2 (#2026)
  • a9e8aab build(deps-dev): bump eslint from 8.41.0 to 8.42.0 (#2024)
  • 37be4ff build(deps-dev): bump @​typescript-eslint/parser from 5.59.8 to 5.59.9 (#2023)
  • a5f0e5d build(deps-dev): bump eslint-plugin-github from 4.7.0 to 4.8.0 (#2025)
  • 9ef70ee build(deps-dev): bump @​types/node from 18.16.14 to 18.16.16 (#2009)
  • 0a28773 build(deps-dev): bump @​typescript-eslint/parser from 5.59.7 to 5.59.8 (#2008)
  • 4ddb8c8 build: update distribution (#1986)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=peter-evans/create-pull-request&package-manager=github_actions&previous-version=3&new-version=5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6a4498e78..8826b5abd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -633,7 +633,7 @@ jobs: - name: Create a PR for maplibre/homebrew-martin if: startsWith(github.ref, 'refs/tags/') - uses: peter-evans/create-pull-request@v3 + uses: peter-evans/create-pull-request@v5 with: # Create a personal access token # Gen: https://github.com/settings/personal-access-tokens/new From 125d78c706a51da7df1cb7aa82908a4e30fcf481 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Thu, 12 Oct 2023 18:51:44 -0400 Subject: [PATCH 049/108] ci cleanup --- .github/dependabot.yml | 27 ------------------------- .github/workflows/build-deploy-docs.yml | 2 +- .github/workflows/dependabot.yml | 6 +++--- 3 files changed, 4 insertions(+), 31 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index c2b051d92..ce9e973a2 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -11,30 +11,3 @@ updates: interval: daily time: "02:00" open-pull-requests-limit: 10 - ignore: - - dependency-name: actix - versions: - - 0.11.0 - - 0.11.1 - - dependency-name: docopt - versions: - - 1.1.1 - - dependency-name: actix-rt - versions: - - 2.0.0 - - 2.0.2 - - 2.1.0 - - 2.2.0 - - dependency-name: postgres - versions: - - 0.19.1 - - dependency-name: serde - versions: - - 1.0.124 - - 1.0.125 - - dependency-name: env_logger - versions: - - 0.8.3 - - dependency-name: criterion - versions: - - 0.3.4 diff --git a/.github/workflows/build-deploy-docs.yml b/.github/workflows/build-deploy-docs.yml index c932c99f0..81d0713ed 100644 --- a/.github/workflows/build-deploy-docs.yml +++ b/.github/workflows/build-deploy-docs.yml @@ -24,7 +24,7 @@ jobs: - name: Deploy uses: peaceiris/actions-gh-pages@v3 - if: ${{ github.ref == 'refs/heads/main' }} + if: github.ref == 'refs/heads/main' with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./target/book diff --git a/.github/workflows/dependabot.yml b/.github/workflows/dependabot.yml index 4e7dfc47f..2a6b0bc5d 100644 --- a/.github/workflows/dependabot.yml +++ b/.github/workflows/dependabot.yml @@ -6,7 +6,7 @@ permissions: write-all jobs: dependabot: runs-on: ubuntu-latest - if: ${{ github.actor == 'dependabot[bot]' }} + if: github.actor == 'dependabot[bot]' steps: - name: Dependabot metadata id: metadata @@ -14,13 +14,13 @@ jobs: with: github-token: "${{ secrets.GITHUB_TOKEN }}" - name: Approve Dependabot PRs - if: ${{steps.metadata.outputs.update-type == 'version-update:semver-patch'}} + if: steps.metadata.outputs.update-type == 'version-update:semver-patch' run: gh pr review --approve "$PR_URL" env: PR_URL: ${{github.event.pull_request.html_url}} GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} - name: Enable auto-merge for Dependabot PRs - if: ${{steps.metadata.outputs.update-type == 'version-update:semver-patch'}} + if: steps.metadata.outputs.update-type == 'version-update:semver-patch' run: gh pr merge --auto --squash "$PR_URL" env: PR_URL: ${{github.event.pull_request.html_url}} From 99b700b1e960c54c8abe1058c622acd7441a1cae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 13 Oct 2023 03:08:03 +0000 Subject: [PATCH 050/108] chore(deps): Bump serde from 1.0.188 to 1.0.189 (#941) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [serde](https://github.com/serde-rs/serde) from 1.0.188 to 1.0.189.
Release notes

Sourced from serde's releases.

v1.0.189

  • Fix "cannot infer type" error when internally tagged enum contains untagged variant (#2613, thanks @​ahl)
Commits
  • e94fc65 Release 1.0.189
  • b908487 Remove double nesting of first_attempt
  • 2a7c7fa Merge pull request #2613 from ahl/fix-untagged-plus-simple
  • e302e15 Merge pull request #2625 from marcospb19/add-csv-to-the-list
  • 1cbea89 Add CSV to the formats list
  • 37a3285 Update ui test suite to nightly-2023-10-06
  • 8c4aad3 Clean up unneeded raw strings in test
  • 1774794 Resolve needless_raw_string_hashes clippy lint in test
  • 1af23f1 Test docs.rs documentation build in CI
  • 94fbc3d fix clippy
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=serde&package-manager=cargo&previous-version=1.0.188&new-version=1.0.189)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0036ae5d0..fddc69410 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2690,18 +2690,18 @@ checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" [[package]] name = "serde" -version = "1.0.188" +version = "1.0.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.188" +version = "1.0.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5" dependencies = [ "proc-macro2", "quote", From d3ef99a27bc4e66cb9331f14e91630ee28fc3e2f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Oct 2023 02:32:44 +0000 Subject: [PATCH 051/108] chore(deps): Bump regex from 1.10.0 to 1.10.1 (#943) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [regex](https://github.com/rust-lang/regex) from 1.10.0 to 1.10.1.
Changelog

Sourced from regex's changelog.

1.10.1 (2023-10-14)

This is a new patch release with a minor increase in the number of valid patterns and a broadening of some literal optimizations.

New features:

  • FEATURE 04f5d7be: Loosen ASCII-compatible rules such that regexes like (?-u:☃) are now allowed.

Performance improvements:

  • PERF 8a8d599f: Broader the reverse suffix optimization to apply in more cases.
Commits
  • 5dff4bd 1.10.1
  • d242ede deps: bump regex-automata to 0.4.2
  • 488604d regex-automata-0.4.2
  • ee01ec2 deps: bump regex-syntax to 0.8.2
  • 1dbeee7 regex-syntax-0.8.2
  • 049d063 changelog: 1.10.1
  • 8a8d599 automata/meta: tweak reverse suffix prefilter strategy
  • 04f5d7b syntax: loosen ASCII compatible rules
  • cfd0ca2 automata/meta: force some prefilter inlining
  • 25ad29f bench: add a redirect
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=regex&package-manager=cargo&previous-version=1.10.0&new-version=1.10.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fddc69410..0b2193d14 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2372,25 +2372,25 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d119d7c7ca818f8a53c300863d4f87566aac09943aef5b355bb83969dae75d87" +checksum = "aaac441002f822bc9705a681810a4dd2963094b9ca0ddc41cb963a4c189189ea" dependencies = [ "aho-corasick", "memchr", "regex-automata", - "regex-syntax 0.8.1", + "regex-syntax 0.8.2", ] [[package]] name = "regex-automata" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "465c6fc0621e4abc4187a2bda0937bfd4f722c2730b29562e19689ea796c9a4b" +checksum = "5011c7e263a695dc8ca064cddb722af1be54e517a280b12a5356f98366899e5d" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.1", + "regex-syntax 0.8.2", ] [[package]] @@ -2401,9 +2401,9 @@ checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" [[package]] name = "regex-syntax" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56d84fdd47036b038fc80dd333d10b6aab10d5d31f4a366e20014def75328d33" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "relative-path" From aeda6e657cd7ee6484113cfe9ee155d13fd7e749 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Oct 2023 02:38:59 +0000 Subject: [PATCH 052/108] chore(deps): Bump flate2 from 1.0.27 to 1.0.28 (#944) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [flate2](https://github.com/rust-lang/flate2-rs) from 1.0.27 to 1.0.28.
Release notes

Sourced from flate2's releases.

1.0.28

What's Changed

New Contributors

Full Changelog: https://github.com/rust-lang/flate2-rs/compare/1.0.27...1.0.28

Commits
  • a99b53e Merge pull request #378 from Byron/prep-release
  • 223f829 Merge pull request #380 from Manishearth/reset-stream
  • 7a61ea5 Reset StreamWrapper after calling mz_inflate / mz_deflate
  • 1260d3e prepare next patch-release
  • f62ff42 Merge pull request #375 from georeth/fix-read-doc
  • 5b23cc9 Fix and unify docs of bufread and read types.
  • f285e9a Merge pull request #373 from anforowicz/fix-spare-capacity-handling
  • 69972b8 Fix soundness of write_to_spare_capacity_of_vec.
  • 82e45fa Refactoring: Dedupe code into write_to_spare_capacity_of_vec helper.
  • 20cdcbe Merge pull request #371 from jongiddy/jgiddy/msrv-1.53
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=flate2&package-manager=cargo&previous-version=1.0.27&new-version=1.0.28)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0b2193d14..abb81386d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1091,9 +1091,9 @@ checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" [[package]] name = "flate2" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" dependencies = [ "crc32fast", "miniz_oxide", From af120188041dff0debb96950a8b7f0caaf7133d2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Oct 2023 02:39:09 +0000 Subject: [PATCH 053/108] chore(deps): Bump async-trait from 0.1.73 to 0.1.74 (#942) Bumps [async-trait](https://github.com/dtolnay/async-trait) from 0.1.73 to 0.1.74.
Release notes

Sourced from async-trait's releases.

0.1.74

  • Documentation improvements
Commits
  • 265979b Release 0.1.74
  • 5e67709 Fix doc test when async fn in trait is natively supported
  • ef144ae Update ui test suite to nightly-2023-10-15
  • 9398a28 Test docs.rs documentation build in CI
  • 8737173 Update ui test suite to nightly-2023-09-24
  • 5ba643c Test dyn Trait containing async fn
  • 247c8e7 Add ui test testing the recommendation to use async-trait
  • 799db66 Update ui test suite to nightly-2023-09-23
  • 0e60248 Update actions/checkout@v3 -> v4
  • 7fcbc83 Update ui test suite to nightly-2023-08-29
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=async-trait&package-manager=cargo&previous-version=0.1.73&new-version=0.1.74)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index abb81386d..690b1a805 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -391,9 +391,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.73" +version = "0.1.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" +checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" dependencies = [ "proc-macro2", "quote", From 00236bf2953886a09efe71e7d9abab9bc7795db5 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Mon, 16 Oct 2023 01:12:58 -0400 Subject: [PATCH 054/108] update lock --- Cargo.lock | 77 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 52 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 690b1a805..7ffb7265b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,7 +11,7 @@ dependencies = [ "actix-macros", "actix-rt", "actix_derive", - "bitflags 2.4.0", + "bitflags 2.4.1", "bytes", "crossbeam-channel", "futures-core", @@ -71,7 +71,7 @@ dependencies = [ "actix-utils", "ahash", "base64", - "bitflags 2.4.0", + "bitflags 2.4.1", "brotli", "bytes", "bytestring", @@ -95,7 +95,7 @@ dependencies = [ "tokio", "tokio-util", "tracing", - "zstd", + "zstd 0.12.4", ] [[package]] @@ -364,9 +364,9 @@ checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" [[package]] name = "async-compression" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb42b2197bf15ccb092b62c74515dbd8b86d0effd934795f6687c93b6e679a2c" +checksum = "f658e2baef915ba0f26f1f7c42bfb8e12f532a01f449a090ded75ae7a07e9ba2" dependencies = [ "brotli", "flate2", @@ -374,8 +374,8 @@ dependencies = [ "memchr", "pin-project-lite", "tokio", - "zstd", - "zstd-safe", + "zstd 0.13.0", + "zstd-safe 7.0.0", ] [[package]] @@ -450,9 +450,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" dependencies = [ "serde", ] @@ -901,9 +901,12 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" +checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3" +dependencies = [ + "powerfmt", +] [[package]] name = "derive_more" @@ -2265,6 +2268,12 @@ dependencies = [ "uuid", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -2518,7 +2527,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.4.1", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -2553,7 +2562,7 @@ version = "0.38.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "745ecfa778e66b2b63c88a61cb36e0eea109e803b0b86bf9879fbc77c70e86ed" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.4.1", "errno", "libc", "linux-raw-sys", @@ -3030,7 +3039,7 @@ checksum = "864b869fdf56263f4c95c45483191ea0af340f9f3e3e7b4d57a61c7c87a970db" dependencies = [ "atoi", "base64", - "bitflags 2.4.0", + "bitflags 2.4.1", "byteorder", "bytes", "crc", @@ -3072,7 +3081,7 @@ checksum = "eb7ae0e6a97fb3ba33b23ac2671a5ce6e3cabe003f451abd5a56e7951d975624" dependencies = [ "atoi", "base64", - "bitflags 2.4.0", + "bitflags 2.4.1", "byteorder", "crc", "dotenvy", @@ -3285,12 +3294,13 @@ dependencies = [ [[package]] name = "time" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "426f806f4089c493dcac0d24c29c01e2c38baf8e30f1b716ee37e83d200b18fe" +checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" dependencies = [ "deranged", "itoa", + "powerfmt", "serde", "time-core", "time-macros", @@ -3478,11 +3488,10 @@ dependencies = [ [[package]] name = "tracing" -version = "0.1.37" +version = "0.1.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +checksum = "ee2ef2af84856a50c1d430afce2fdded0a4ec7eda868db86409b4543df0797f9" dependencies = [ - "cfg-if", "log", "pin-project-lite", "tracing-attributes", @@ -3491,9 +3500,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", @@ -3502,9 +3511,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.31" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", ] @@ -4044,7 +4053,16 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a27595e173641171fc74a1232b7b1c7a7cb6e18222c11e9dfb9888fa424c53c" dependencies = [ - "zstd-safe", + "zstd-safe 6.0.6", +] + +[[package]] +name = "zstd" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bffb3309596d527cfcba7dfc6ed6052f1d39dfbd7c867aa2e865e4a449c10110" +dependencies = [ + "zstd-safe 7.0.0", ] [[package]] @@ -4057,6 +4075,15 @@ dependencies = [ "zstd-sys", ] +[[package]] +name = "zstd-safe" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43747c7422e2924c11144d5229878b98180ef8b06cca4ab5af37afc8a8d8ea3e" +dependencies = [ + "zstd-sys", +] + [[package]] name = "zstd-sys" version = "2.0.9+zstd.1.5.5" From e2a26fe5199b198e9b1319da283c602ca548d119 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Mon, 16 Oct 2023 01:17:45 -0400 Subject: [PATCH 055/108] more test coverage for mbtiles --- martin-mbtiles/tests/mbtiles.rs | 25 +++++++++++++------ .../mbtiles__databases@flat__dif.snap | 7 ++++-- .../mbtiles__databases@flat__v1-no-hash.snap | 4 +++ .../mbtiles__databases@flat__v1.snap | 6 ++++- .../mbtiles__databases@flat__v2.snap | 5 +++- .../mbtiles__databases@hash__dif.snap | 7 ++++-- .../mbtiles__databases@hash__v1-no-hash.snap | 4 +++ .../mbtiles__databases@hash__v1.snap | 6 ++++- .../mbtiles__databases@hash__v2.snap | 5 +++- .../mbtiles__databases@norm__dif.snap | 9 +++++-- .../mbtiles__databases@norm__v1-no-hash.snap | 6 +++++ .../mbtiles__databases@norm__v1.snap | 8 +++++- .../mbtiles__databases@norm__v2.snap | 7 +++++- 13 files changed, 80 insertions(+), 19 deletions(-) diff --git a/martin-mbtiles/tests/mbtiles.rs b/martin-mbtiles/tests/mbtiles.rs index a152b8613..833452b9e 100644 --- a/martin-mbtiles/tests/mbtiles.rs +++ b/martin-mbtiles/tests/mbtiles.rs @@ -17,8 +17,12 @@ const TILES_V1: &str = " INSERT INTO tiles (zoom_level, tile_column, tile_row, tile_data) VALUES --(z, x, y, data) -- rules: keep if x=0, edit if x=1, remove if x=2 (5, 0, 0, cast('same' as blob)) + , (5, 0, 1, cast('' as blob)) -- empty tile, keep , (5, 1, 1, cast('edit-v1' as blob)) + , (5, 1, 2, cast('' as blob)) -- empty tile, edit + , (5, 1, 3, cast('non-empty' as blob)) -- non empty tile to edit , (5, 2, 2, cast('remove' as blob)) + , (5, 2, 3, cast('' as blob)) -- empty tile, remove , (6, 0, 3, cast('same' as blob)) , (6, 1, 4, cast('edit-v1' as blob)) , (6, 0, 5, cast('1-keep-1-rm' as blob)) @@ -28,18 +32,25 @@ const TILES_V1: &str = " const TILES_V2: &str = " INSERT INTO tiles (zoom_level, tile_column, tile_row, tile_data) VALUES (5, 0, 0, cast('same' as blob)) -- no changes + , (5, 0, 1, cast('' as blob)) -- no changes, empty tile , (5, 1, 1, cast('edit-v2' as blob)) -- edited in-place - -- , (5, 2, 2, cast('remove' as blob)) -- this row is deleted + , (5, 1, 2, cast('not-empty' as blob)) -- edited in-place, replaced empty with non-empty + , (5, 1, 3, cast('' as blob)) -- edited in-place, replaced non-empty with empty + -- , (5, 2, 2, cast('remove' as blob)) -- this row is removed + -- , (5, 2, 3, cast('' as blob)) -- empty tile, removed , (6, 0, 3, cast('same' as blob)) -- no changes, same content as 5/0/0 , (6, 1, 4, cast('edit-v2a' as blob)) -- edited, used to be same as 5/1/1 , (6, 0, 5, cast('1-keep-1-rm' as blob)) -- this row is kept (same content as next) - -- , (6, 2, 6, cast('1-keep-1-rm' as blob)) -- this row is deleted + -- , (6, 2, 6, cast('1-keep-1-rm' as blob)) -- this row is removed , (5, 3, 7, cast('new' as blob)) -- this row is added, dup value , (5, 3, 8, cast('new' as blob)) -- this row is added, dup value -- Expected delta: -- 5/1/1 edit + -- 5/1/2 edit + -- 5/1/3 edit -- 5/2/2 remove + -- 5/2/3 remove -- 5/3/7 add -- 5/3/8 add -- 6/1/4 edit @@ -167,7 +178,7 @@ fn databases() -> Databases { let dmp = assert_dump!(&mut v1_cn, "{typ}__v1"); let hash = v1_mbt.validate(Off, false).await.unwrap(); allow_duplicates! { - assert_display_snapshot!(hash, @"0063DADF9C78A376418DB0D2B00A5F80"); + assert_display_snapshot!(hash, @"096A8399D486CF443A5DF0CEC1AD8BB2"); } result.insert(("v1", mbt_typ), dmp); @@ -176,7 +187,7 @@ fn databases() -> Databases { let dmp = assert_dump!(&mut v2_cn, "{typ}__v2"); let hash = v2_mbt.validate(Off, false).await.unwrap(); allow_duplicates! { - assert_display_snapshot!(hash, @"5C90855D70120501451BDD08CA71341A"); + assert_display_snapshot!(hash, @"FE0D3090E8B4E89F2C755C08E8D76BEA"); } result.insert(("v2", mbt_typ), dmp); @@ -187,7 +198,7 @@ fn databases() -> Databases { let dmp = assert_dump!(&mut dif_cn, "{typ}__dif"); let hash = dif_mbt.validate(Off, false).await.unwrap(); allow_duplicates! { - assert_display_snapshot!(hash, @"AB9EE21538C1D28BB357ABB3A45BD6BD"); + assert_display_snapshot!(hash, @"B86122579EDCDD4C51F3910894FCC1A1"); } result.insert(("dif", mbt_typ), dmp); } @@ -274,7 +285,7 @@ async fn diff_apply( apply_patch(path(&tar1_mbt), path(&dif_mbt)).await?; let hash_v1 = tar1_mbt.validate(Off, false).await?; allow_duplicates! { - assert_display_snapshot!(hash_v1, @"5C90855D70120501451BDD08CA71341A"); + assert_display_snapshot!(hash_v1, @"FE0D3090E8B4E89F2C755C08E8D76BEA"); } let dmp = dump(&mut tar1_cn).await?; pretty_assert_eq!(&dmp, expected_v2); @@ -285,7 +296,7 @@ async fn diff_apply( apply_patch(path(&tar2_mbt), path(&dif_mbt)).await?; let hash_v2 = tar2_mbt.validate(Off, false).await?; allow_duplicates! { - assert_display_snapshot!(hash_v2, @"5C90855D70120501451BDD08CA71341A"); + assert_display_snapshot!(hash_v2, @"FE0D3090E8B4E89F2C755C08E8D76BEA"); } let dmp = dump(&mut tar2_cn).await?; pretty_assert_eq!(&dmp, expected_v2); diff --git a/martin-mbtiles/tests/snapshots/mbtiles__databases@flat__dif.snap b/martin-mbtiles/tests/snapshots/mbtiles__databases@flat__dif.snap index e39c786f7..84ccfd9e1 100644 --- a/martin-mbtiles/tests/snapshots/mbtiles__databases@flat__dif.snap +++ b/martin-mbtiles/tests/snapshots/mbtiles__databases@flat__dif.snap @@ -10,8 +10,8 @@ CREATE TABLE metadata ( name text NOT NULL PRIMARY KEY, value text)''' values = [ - '( "agg_tiles_hash", "AB9EE21538C1D28BB357ABB3A45BD6BD" )', - '( "agg_tiles_hash_after_apply", "5C90855D70120501451BDD08CA71341A" )', + '( "agg_tiles_hash", "B86122579EDCDD4C51F3910894FCC1A1" )', + '( "agg_tiles_hash_after_apply", "FE0D3090E8B4E89F2C755C08E8D76BEA" )', '( "md-edit", "value - v2" )', '( "md-new", "value - new" )', '( "md-remove", NULL )', @@ -29,7 +29,10 @@ CREATE TABLE tiles ( PRIMARY KEY(zoom_level, tile_column, tile_row))''' values = [ '( 5, 1, 1, blob(edit-v2) )', + '( 5, 1, 2, blob(not-empty) )', + '( 5, 1, 3, blob() )', '( 5, 2, 2, NULL )', + '( 5, 2, 3, NULL )', '( 5, 3, 7, blob(new) )', '( 5, 3, 8, blob(new) )', '( 6, 1, 4, blob(edit-v2a) )', diff --git a/martin-mbtiles/tests/snapshots/mbtiles__databases@flat__v1-no-hash.snap b/martin-mbtiles/tests/snapshots/mbtiles__databases@flat__v1-no-hash.snap index c0ede5eaa..613d2e8ef 100644 --- a/martin-mbtiles/tests/snapshots/mbtiles__databases@flat__v1-no-hash.snap +++ b/martin-mbtiles/tests/snapshots/mbtiles__databases@flat__v1-no-hash.snap @@ -27,8 +27,12 @@ CREATE TABLE tiles ( PRIMARY KEY(zoom_level, tile_column, tile_row))''' values = [ '( 5, 0, 0, blob(same) )', + '( 5, 0, 1, blob() )', '( 5, 1, 1, blob(edit-v1) )', + '( 5, 1, 2, blob() )', + '( 5, 1, 3, blob(non-empty) )', '( 5, 2, 2, blob(remove) )', + '( 5, 2, 3, blob() )', '( 6, 0, 3, blob(same) )', '( 6, 0, 5, blob(1-keep-1-rm) )', '( 6, 1, 4, blob(edit-v1) )', diff --git a/martin-mbtiles/tests/snapshots/mbtiles__databases@flat__v1.snap b/martin-mbtiles/tests/snapshots/mbtiles__databases@flat__v1.snap index 48b9f31cf..cccaf7fb0 100644 --- a/martin-mbtiles/tests/snapshots/mbtiles__databases@flat__v1.snap +++ b/martin-mbtiles/tests/snapshots/mbtiles__databases@flat__v1.snap @@ -10,7 +10,7 @@ CREATE TABLE metadata ( name text NOT NULL PRIMARY KEY, value text)''' values = [ - '( "agg_tiles_hash", "0063DADF9C78A376418DB0D2B00A5F80" )', + '( "agg_tiles_hash", "096A8399D486CF443A5DF0CEC1AD8BB2" )', '( "md-edit", "value - v1" )', '( "md-remove", "value - remove" )', '( "md-same", "value - same" )', @@ -28,8 +28,12 @@ CREATE TABLE tiles ( PRIMARY KEY(zoom_level, tile_column, tile_row))''' values = [ '( 5, 0, 0, blob(same) )', + '( 5, 0, 1, blob() )', '( 5, 1, 1, blob(edit-v1) )', + '( 5, 1, 2, blob() )', + '( 5, 1, 3, blob(non-empty) )', '( 5, 2, 2, blob(remove) )', + '( 5, 2, 3, blob() )', '( 6, 0, 3, blob(same) )', '( 6, 0, 5, blob(1-keep-1-rm) )', '( 6, 1, 4, blob(edit-v1) )', diff --git a/martin-mbtiles/tests/snapshots/mbtiles__databases@flat__v2.snap b/martin-mbtiles/tests/snapshots/mbtiles__databases@flat__v2.snap index e316a7461..c3b44ccb6 100644 --- a/martin-mbtiles/tests/snapshots/mbtiles__databases@flat__v2.snap +++ b/martin-mbtiles/tests/snapshots/mbtiles__databases@flat__v2.snap @@ -10,7 +10,7 @@ CREATE TABLE metadata ( name text NOT NULL PRIMARY KEY, value text)''' values = [ - '( "agg_tiles_hash", "5C90855D70120501451BDD08CA71341A" )', + '( "agg_tiles_hash", "FE0D3090E8B4E89F2C755C08E8D76BEA" )', '( "md-edit", "value - v2" )', '( "md-new", "value - new" )', '( "md-same", "value - same" )', @@ -28,7 +28,10 @@ CREATE TABLE tiles ( PRIMARY KEY(zoom_level, tile_column, tile_row))''' values = [ '( 5, 0, 0, blob(same) )', + '( 5, 0, 1, blob() )', '( 5, 1, 1, blob(edit-v2) )', + '( 5, 1, 2, blob(not-empty) )', + '( 5, 1, 3, blob() )', '( 5, 3, 7, blob(new) )', '( 5, 3, 8, blob(new) )', '( 6, 0, 3, blob(same) )', diff --git a/martin-mbtiles/tests/snapshots/mbtiles__databases@hash__dif.snap b/martin-mbtiles/tests/snapshots/mbtiles__databases@hash__dif.snap index e3dcecf0c..7d01a84f9 100644 --- a/martin-mbtiles/tests/snapshots/mbtiles__databases@hash__dif.snap +++ b/martin-mbtiles/tests/snapshots/mbtiles__databases@hash__dif.snap @@ -10,8 +10,8 @@ CREATE TABLE metadata ( name text NOT NULL PRIMARY KEY, value text)''' values = [ - '( "agg_tiles_hash", "AB9EE21538C1D28BB357ABB3A45BD6BD" )', - '( "agg_tiles_hash_after_apply", "5C90855D70120501451BDD08CA71341A" )', + '( "agg_tiles_hash", "B86122579EDCDD4C51F3910894FCC1A1" )', + '( "agg_tiles_hash_after_apply", "FE0D3090E8B4E89F2C755C08E8D76BEA" )', '( "md-edit", "value - v2" )', '( "md-new", "value - new" )', '( "md-remove", NULL )', @@ -30,7 +30,10 @@ CREATE TABLE tiles_with_hash ( PRIMARY KEY(zoom_level, tile_column, tile_row))''' values = [ '( 5, 1, 1, blob(edit-v2), "FF76830FF90D79BB335884F256031731" )', + '( 5, 1, 2, blob(not-empty), "99DEE0E66806ECF1C20C09F64B2C0A34" )', + '( 5, 1, 3, blob(), "D41D8CD98F00B204E9800998ECF8427E" )', '( 5, 2, 2, NULL, "" )', + '( 5, 2, 3, NULL, "" )', '( 5, 3, 7, blob(new), "22AF645D1859CB5CA6DA0C484F1F37EA" )', '( 5, 3, 8, blob(new), "22AF645D1859CB5CA6DA0C484F1F37EA" )', '( 6, 1, 4, blob(edit-v2a), "03132BFACDB00CC63D6B7DD98D974DD5" )', diff --git a/martin-mbtiles/tests/snapshots/mbtiles__databases@hash__v1-no-hash.snap b/martin-mbtiles/tests/snapshots/mbtiles__databases@hash__v1-no-hash.snap index 1c18d1329..db7f84fd9 100644 --- a/martin-mbtiles/tests/snapshots/mbtiles__databases@hash__v1-no-hash.snap +++ b/martin-mbtiles/tests/snapshots/mbtiles__databases@hash__v1-no-hash.snap @@ -28,8 +28,12 @@ CREATE TABLE tiles_with_hash ( PRIMARY KEY(zoom_level, tile_column, tile_row))''' values = [ '( 5, 0, 0, blob(same), "51037A4A37730F52C8732586D3AAA316" )', + '( 5, 0, 1, blob(), "D41D8CD98F00B204E9800998ECF8427E" )', '( 5, 1, 1, blob(edit-v1), "EFE0AE5FD114DE99855BC2838BE97E1D" )', + '( 5, 1, 2, blob(), "D41D8CD98F00B204E9800998ECF8427E" )', + '( 5, 1, 3, blob(non-empty), "720C02778717818CC0A869955BA2AFB6" )', '( 5, 2, 2, blob(remove), "0F6969D7052DA9261E31DDB6E88C136E" )', + '( 5, 2, 3, blob(), "D41D8CD98F00B204E9800998ECF8427E" )', '( 6, 0, 3, blob(same), "51037A4A37730F52C8732586D3AAA316" )', '( 6, 0, 5, blob(1-keep-1-rm), "535A5575B48444EDEB926815AB26EC9B" )', '( 6, 1, 4, blob(edit-v1), "EFE0AE5FD114DE99855BC2838BE97E1D" )', diff --git a/martin-mbtiles/tests/snapshots/mbtiles__databases@hash__v1.snap b/martin-mbtiles/tests/snapshots/mbtiles__databases@hash__v1.snap index bce478b04..0668dab3e 100644 --- a/martin-mbtiles/tests/snapshots/mbtiles__databases@hash__v1.snap +++ b/martin-mbtiles/tests/snapshots/mbtiles__databases@hash__v1.snap @@ -10,7 +10,7 @@ CREATE TABLE metadata ( name text NOT NULL PRIMARY KEY, value text)''' values = [ - '( "agg_tiles_hash", "0063DADF9C78A376418DB0D2B00A5F80" )', + '( "agg_tiles_hash", "096A8399D486CF443A5DF0CEC1AD8BB2" )', '( "md-edit", "value - v1" )', '( "md-remove", "value - remove" )', '( "md-same", "value - same" )', @@ -29,8 +29,12 @@ CREATE TABLE tiles_with_hash ( PRIMARY KEY(zoom_level, tile_column, tile_row))''' values = [ '( 5, 0, 0, blob(same), "51037A4A37730F52C8732586D3AAA316" )', + '( 5, 0, 1, blob(), "D41D8CD98F00B204E9800998ECF8427E" )', '( 5, 1, 1, blob(edit-v1), "EFE0AE5FD114DE99855BC2838BE97E1D" )', + '( 5, 1, 2, blob(), "D41D8CD98F00B204E9800998ECF8427E" )', + '( 5, 1, 3, blob(non-empty), "720C02778717818CC0A869955BA2AFB6" )', '( 5, 2, 2, blob(remove), "0F6969D7052DA9261E31DDB6E88C136E" )', + '( 5, 2, 3, blob(), "D41D8CD98F00B204E9800998ECF8427E" )', '( 6, 0, 3, blob(same), "51037A4A37730F52C8732586D3AAA316" )', '( 6, 0, 5, blob(1-keep-1-rm), "535A5575B48444EDEB926815AB26EC9B" )', '( 6, 1, 4, blob(edit-v1), "EFE0AE5FD114DE99855BC2838BE97E1D" )', diff --git a/martin-mbtiles/tests/snapshots/mbtiles__databases@hash__v2.snap b/martin-mbtiles/tests/snapshots/mbtiles__databases@hash__v2.snap index d794b6e95..d4cedb545 100644 --- a/martin-mbtiles/tests/snapshots/mbtiles__databases@hash__v2.snap +++ b/martin-mbtiles/tests/snapshots/mbtiles__databases@hash__v2.snap @@ -10,7 +10,7 @@ CREATE TABLE metadata ( name text NOT NULL PRIMARY KEY, value text)''' values = [ - '( "agg_tiles_hash", "5C90855D70120501451BDD08CA71341A" )', + '( "agg_tiles_hash", "FE0D3090E8B4E89F2C755C08E8D76BEA" )', '( "md-edit", "value - v2" )', '( "md-new", "value - new" )', '( "md-same", "value - same" )', @@ -29,7 +29,10 @@ CREATE TABLE tiles_with_hash ( PRIMARY KEY(zoom_level, tile_column, tile_row))''' values = [ '( 5, 0, 0, blob(same), "51037A4A37730F52C8732586D3AAA316" )', + '( 5, 0, 1, blob(), "D41D8CD98F00B204E9800998ECF8427E" )', '( 5, 1, 1, blob(edit-v2), "FF76830FF90D79BB335884F256031731" )', + '( 5, 1, 2, blob(not-empty), "99DEE0E66806ECF1C20C09F64B2C0A34" )', + '( 5, 1, 3, blob(), "D41D8CD98F00B204E9800998ECF8427E" )', '( 5, 3, 7, blob(new), "22AF645D1859CB5CA6DA0C484F1F37EA" )', '( 5, 3, 8, blob(new), "22AF645D1859CB5CA6DA0C484F1F37EA" )', '( 6, 0, 3, blob(same), "51037A4A37730F52C8732586D3AAA316" )', diff --git a/martin-mbtiles/tests/snapshots/mbtiles__databases@norm__dif.snap b/martin-mbtiles/tests/snapshots/mbtiles__databases@norm__dif.snap index e2fe35f9d..cad93944a 100644 --- a/martin-mbtiles/tests/snapshots/mbtiles__databases@norm__dif.snap +++ b/martin-mbtiles/tests/snapshots/mbtiles__databases@norm__dif.snap @@ -13,6 +13,8 @@ values = [ '( "", NULL )', '( "03132BFACDB00CC63D6B7DD98D974DD5", blob(edit-v2a) )', '( "22AF645D1859CB5CA6DA0C484F1F37EA", blob(new) )', + '( "99DEE0E66806ECF1C20C09F64B2C0A34", blob(not-empty) )', + '( "D41D8CD98F00B204E9800998ECF8427E", blob() )', '( "FF76830FF90D79BB335884F256031731", blob(edit-v2) )', ] @@ -28,7 +30,10 @@ CREATE TABLE map ( PRIMARY KEY(zoom_level, tile_column, tile_row))''' values = [ '( 5, 1, 1, "FF76830FF90D79BB335884F256031731" )', + '( 5, 1, 2, "99DEE0E66806ECF1C20C09F64B2C0A34" )', + '( 5, 1, 3, "D41D8CD98F00B204E9800998ECF8427E" )', '( 5, 2, 2, "" )', + '( 5, 2, 3, "" )', '( 5, 3, 7, "22AF645D1859CB5CA6DA0C484F1F37EA" )', '( 5, 3, 8, "22AF645D1859CB5CA6DA0C484F1F37EA" )', '( 6, 1, 4, "03132BFACDB00CC63D6B7DD98D974DD5" )', @@ -43,8 +48,8 @@ CREATE TABLE metadata ( name text NOT NULL PRIMARY KEY, value text)''' values = [ - '( "agg_tiles_hash", "AB9EE21538C1D28BB357ABB3A45BD6BD" )', - '( "agg_tiles_hash_after_apply", "5C90855D70120501451BDD08CA71341A" )', + '( "agg_tiles_hash", "B86122579EDCDD4C51F3910894FCC1A1" )', + '( "agg_tiles_hash_after_apply", "FE0D3090E8B4E89F2C755C08E8D76BEA" )', '( "md-edit", "value - v2" )', '( "md-new", "value - new" )', '( "md-remove", NULL )', diff --git a/martin-mbtiles/tests/snapshots/mbtiles__databases@norm__v1-no-hash.snap b/martin-mbtiles/tests/snapshots/mbtiles__databases@norm__v1-no-hash.snap index 3586bffb1..62067b4a5 100644 --- a/martin-mbtiles/tests/snapshots/mbtiles__databases@norm__v1-no-hash.snap +++ b/martin-mbtiles/tests/snapshots/mbtiles__databases@norm__v1-no-hash.snap @@ -13,6 +13,8 @@ values = [ '( "0F6969D7052DA9261E31DDB6E88C136E", blob(remove) )', '( "51037A4A37730F52C8732586D3AAA316", blob(same) )', '( "535A5575B48444EDEB926815AB26EC9B", blob(1-keep-1-rm) )', + '( "720C02778717818CC0A869955BA2AFB6", blob(non-empty) )', + '( "D41D8CD98F00B204E9800998ECF8427E", blob() )', '( "EFE0AE5FD114DE99855BC2838BE97E1D", blob(edit-v1) )', ] @@ -28,8 +30,12 @@ CREATE TABLE map ( PRIMARY KEY(zoom_level, tile_column, tile_row))''' values = [ '( 5, 0, 0, "51037A4A37730F52C8732586D3AAA316" )', + '( 5, 0, 1, "D41D8CD98F00B204E9800998ECF8427E" )', '( 5, 1, 1, "EFE0AE5FD114DE99855BC2838BE97E1D" )', + '( 5, 1, 2, "D41D8CD98F00B204E9800998ECF8427E" )', + '( 5, 1, 3, "720C02778717818CC0A869955BA2AFB6" )', '( 5, 2, 2, "0F6969D7052DA9261E31DDB6E88C136E" )', + '( 5, 2, 3, "D41D8CD98F00B204E9800998ECF8427E" )', '( 6, 0, 3, "51037A4A37730F52C8732586D3AAA316" )', '( 6, 0, 5, "535A5575B48444EDEB926815AB26EC9B" )', '( 6, 1, 4, "EFE0AE5FD114DE99855BC2838BE97E1D" )', diff --git a/martin-mbtiles/tests/snapshots/mbtiles__databases@norm__v1.snap b/martin-mbtiles/tests/snapshots/mbtiles__databases@norm__v1.snap index ea71fb5f8..5ee4cf9e4 100644 --- a/martin-mbtiles/tests/snapshots/mbtiles__databases@norm__v1.snap +++ b/martin-mbtiles/tests/snapshots/mbtiles__databases@norm__v1.snap @@ -13,6 +13,8 @@ values = [ '( "0F6969D7052DA9261E31DDB6E88C136E", blob(remove) )', '( "51037A4A37730F52C8732586D3AAA316", blob(same) )', '( "535A5575B48444EDEB926815AB26EC9B", blob(1-keep-1-rm) )', + '( "720C02778717818CC0A869955BA2AFB6", blob(non-empty) )', + '( "D41D8CD98F00B204E9800998ECF8427E", blob() )', '( "EFE0AE5FD114DE99855BC2838BE97E1D", blob(edit-v1) )', ] @@ -28,8 +30,12 @@ CREATE TABLE map ( PRIMARY KEY(zoom_level, tile_column, tile_row))''' values = [ '( 5, 0, 0, "51037A4A37730F52C8732586D3AAA316" )', + '( 5, 0, 1, "D41D8CD98F00B204E9800998ECF8427E" )', '( 5, 1, 1, "EFE0AE5FD114DE99855BC2838BE97E1D" )', + '( 5, 1, 2, "D41D8CD98F00B204E9800998ECF8427E" )', + '( 5, 1, 3, "720C02778717818CC0A869955BA2AFB6" )', '( 5, 2, 2, "0F6969D7052DA9261E31DDB6E88C136E" )', + '( 5, 2, 3, "D41D8CD98F00B204E9800998ECF8427E" )', '( 6, 0, 3, "51037A4A37730F52C8732586D3AAA316" )', '( 6, 0, 5, "535A5575B48444EDEB926815AB26EC9B" )', '( 6, 1, 4, "EFE0AE5FD114DE99855BC2838BE97E1D" )', @@ -44,7 +50,7 @@ CREATE TABLE metadata ( name text NOT NULL PRIMARY KEY, value text)''' values = [ - '( "agg_tiles_hash", "0063DADF9C78A376418DB0D2B00A5F80" )', + '( "agg_tiles_hash", "096A8399D486CF443A5DF0CEC1AD8BB2" )', '( "md-edit", "value - v1" )', '( "md-remove", "value - remove" )', '( "md-same", "value - same" )', diff --git a/martin-mbtiles/tests/snapshots/mbtiles__databases@norm__v2.snap b/martin-mbtiles/tests/snapshots/mbtiles__databases@norm__v2.snap index 46b88cdcb..2bd5bbeb5 100644 --- a/martin-mbtiles/tests/snapshots/mbtiles__databases@norm__v2.snap +++ b/martin-mbtiles/tests/snapshots/mbtiles__databases@norm__v2.snap @@ -14,6 +14,8 @@ values = [ '( "22AF645D1859CB5CA6DA0C484F1F37EA", blob(new) )', '( "51037A4A37730F52C8732586D3AAA316", blob(same) )', '( "535A5575B48444EDEB926815AB26EC9B", blob(1-keep-1-rm) )', + '( "99DEE0E66806ECF1C20C09F64B2C0A34", blob(not-empty) )', + '( "D41D8CD98F00B204E9800998ECF8427E", blob() )', '( "FF76830FF90D79BB335884F256031731", blob(edit-v2) )', ] @@ -29,7 +31,10 @@ CREATE TABLE map ( PRIMARY KEY(zoom_level, tile_column, tile_row))''' values = [ '( 5, 0, 0, "51037A4A37730F52C8732586D3AAA316" )', + '( 5, 0, 1, "D41D8CD98F00B204E9800998ECF8427E" )', '( 5, 1, 1, "FF76830FF90D79BB335884F256031731" )', + '( 5, 1, 2, "99DEE0E66806ECF1C20C09F64B2C0A34" )', + '( 5, 1, 3, "D41D8CD98F00B204E9800998ECF8427E" )', '( 5, 3, 7, "22AF645D1859CB5CA6DA0C484F1F37EA" )', '( 5, 3, 8, "22AF645D1859CB5CA6DA0C484F1F37EA" )', '( 6, 0, 3, "51037A4A37730F52C8732586D3AAA316" )', @@ -45,7 +50,7 @@ CREATE TABLE metadata ( name text NOT NULL PRIMARY KEY, value text)''' values = [ - '( "agg_tiles_hash", "5C90855D70120501451BDD08CA71341A" )', + '( "agg_tiles_hash", "FE0D3090E8B4E89F2C755C08E8D76BEA" )', '( "md-edit", "value - v2" )', '( "md-new", "value - new" )', '( "md-same", "value - same" )', From a1f794ae4a23a96185d1b4b2eb856f946012b2b4 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Mon, 16 Oct 2023 12:40:44 -0400 Subject: [PATCH 056/108] mbtiles: Add `--apply-patch` to copy, rename apply-diff (#945) * `mbtiles apply-diff` is now `apply-patch` (old name is still supported) * `mbtiles copy` can now take `--apply-patch ` to apply the patch while copying from source to destination. This way, the source file will remain unmodified. --- martin-mbtiles/src/bin/main.rs | 10 +- martin-mbtiles/src/copier.rs | 320 ++++++++++++++++++++++++-------- martin-mbtiles/src/errors.rs | 8 + martin-mbtiles/src/lib.rs | 4 +- martin-mbtiles/src/mbtiles.rs | 39 +++- martin-mbtiles/src/patcher.rs | 6 +- martin-mbtiles/tests/mbtiles.rs | 201 +++++++++++++------- tests/expected/mbtiles/help.txt | 14 +- 8 files changed, 427 insertions(+), 175 deletions(-) diff --git a/martin-mbtiles/src/bin/main.rs b/martin-mbtiles/src/bin/main.rs index 5dd16c9b1..8842a64b3 100644 --- a/martin-mbtiles/src/bin/main.rs +++ b/martin-mbtiles/src/bin/main.rs @@ -48,8 +48,8 @@ enum Commands { #[command(name = "copy")] Copy(MbtilesCopier), /// Apply diff file generated from 'copy' command - #[command(name = "apply-diff")] - ApplyDiff { + #[command(name = "apply-patch", alias = "apply-diff")] + ApplyPatch { /// MBTiles file to apply diff to src_file: PathBuf, /// Diff file @@ -100,7 +100,7 @@ async fn main_int() -> anyhow::Result<()> { Commands::Copy(opts) => { opts.run().await?; } - Commands::ApplyDiff { + Commands::ApplyPatch { src_file, diff_file, } => { @@ -150,7 +150,7 @@ mod tests { use clap::Parser; use martin_mbtiles::{CopyDuplicateMode, MbtilesCopier}; - use crate::Commands::{ApplyDiff, Copy, MetaGetValue, MetaSetValue, Validate}; + use crate::Commands::{ApplyPatch, Copy, MetaGetValue, MetaSetValue, Validate}; use crate::{Args, IntegrityCheckType}; #[test] @@ -410,7 +410,7 @@ mod tests { Args::parse_from(["mbtiles", "apply-diff", "src_file", "diff_file"]), Args { verbose: false, - command: ApplyDiff { + command: ApplyPatch { src_file: PathBuf::from("src_file"), diff_file: PathBuf::from("diff_file"), } diff --git a/martin-mbtiles/src/copier.rs b/martin-mbtiles/src/copier.rs index 3af7626a3..d8c165698 100644 --- a/martin-mbtiles/src/copier.rs +++ b/martin-mbtiles/src/copier.rs @@ -10,8 +10,8 @@ use sqlite_hashes::rusqlite::params_from_iter; use sqlx::{query, Executor as _, Row, SqliteConnection}; use crate::errors::MbtResult; -use crate::mbtiles::MbtType; use crate::mbtiles::MbtType::{Flat, FlatWithHash, Normalized}; +use crate::mbtiles::{MbtType, MbtTypeCli}; use crate::queries::{ create_flat_tables, create_flat_with_hash_tables, create_normalized_tables, create_tiles_with_hash_view, detach_db, is_empty_database, @@ -36,7 +36,10 @@ pub struct MbtilesCopier { /// MBTiles file to write to pub dst_file: PathBuf, /// Output format of the destination file, ignored if the file exists. If not specified, defaults to the type of source - #[cfg_attr(feature = "cli", arg(long, value_enum))] + #[cfg_attr(feature = "cli", arg(long = "dst_type", value_enum))] + pub dst_type_cli: Option, + /// Destination type with options + #[cfg_attr(feature = "cli", arg(skip))] pub dst_type: Option, /// Specify copying behaviour when tiles with duplicate (zoom_level, tile_column, tile_row) values are found #[cfg_attr(feature = "cli", arg(long, value_enum, default_value_t = CopyDuplicateMode::default()))] @@ -52,8 +55,12 @@ pub struct MbtilesCopier { pub zoom_levels: HashSet, /// Compare source file with this file, and only copy non-identical tiles to destination. /// It should be later possible to run `mbtiles apply-diff SRC_FILE DST_FILE` to get the same DIFF file. - #[cfg_attr(feature = "cli", arg(long))] + #[cfg_attr(feature = "cli", arg(long, conflicts_with("apply_patch")))] pub diff_with_file: Option, + /// Compare source file with this file, and only copy non-identical tiles to destination. + /// It should be later possible to run `mbtiles apply-diff SRC_FILE DST_FILE` to get the same DIFF file. + #[cfg_attr(feature = "cli", arg(long, conflicts_with("diff_with_file")))] + pub apply_patch: Option, /// Skip generating a global hash for mbtiles validation. By default, `mbtiles` will compute `agg_tiles_hash` metadata value. #[cfg_attr(feature = "cli", arg(long))] pub skip_agg_tiles_hash: bool, @@ -105,11 +112,13 @@ impl MbtilesCopier { src_file: src_filepath, dst_file: dst_filepath, zoom_levels: HashSet::new(), + dst_type_cli: None, dst_type: None, on_duplicate: CopyDuplicateMode::Override, min_zoom: None, max_zoom: None, diff_with_file: None, + apply_patch: None, skip_agg_tiles_hash: false, } } @@ -117,10 +126,23 @@ impl MbtilesCopier { pub async fn run(self) -> MbtResult { MbtileCopierInt::new(self)?.run().await } + + pub(crate) fn dst_type(&self) -> Option { + self.dst_type.or_else(|| { + self.dst_type_cli.map(|t| match t { + MbtTypeCli::Flat => Flat, + MbtTypeCli::FlatWithHash => FlatWithHash, + MbtTypeCli::Normalized => Normalized { hash_view: true }, + }) + }) + } } impl MbtileCopierInt { pub fn new(options: MbtilesCopier) -> MbtResult { + if options.apply_patch.is_some() && options.diff_with_file.is_some() { + return Err(MbtError::CannotApplyPatchAndDiff); + } // We may want to resolve the files to absolute paths here, but will need to avoid various non-file cases if options.src_file == options.dst_file { return Err(MbtError::SameSourceAndDestination(options.src_file)); @@ -130,6 +152,12 @@ impl MbtileCopierInt { return Err(MbtError::SameDiffAndSourceOrDestination(options.src_file)); } } + if let Some(patch_file) = &options.apply_patch { + if options.src_file == *patch_file || options.dst_file == *patch_file { + return Err(MbtError::SameDiffAndSourceOrDestination(options.src_file)); + } + } + Ok(MbtileCopierInt { src_mbtiles: Mbtiles::new(&options.src_file)?, dst_mbtiles: Mbtiles::new(&options.dst_file)?, @@ -138,37 +166,43 @@ impl MbtileCopierInt { } pub async fn run(self) -> MbtResult { + let dif = match (&self.options.diff_with_file, &self.options.apply_patch) { + (Some(dif_file), None) | (None, Some(dif_file)) => { + let dif_mbt = Mbtiles::new(dif_file)?; + let dif_type = dif_mbt.open_and_detect_type().await?; + Some((dif_mbt, dif_type, dif_type)) + } + (Some(_), Some(_)) => unreachable!(), // validated in the Self::new + _ => None, + }; + // src and diff file connections are not needed later, as they will be attached to the dst file let src_mbt = &self.src_mbtiles; let dst_mbt = &self.dst_mbtiles; let src_type = src_mbt.open_and_detect_type().await?; - let dif = if let Some(dif_file) = &self.options.diff_with_file { - let dif_file = Mbtiles::new(dif_file)?; - let dif_type = dif_file.open_and_detect_type().await?; - Some((dif_file, dif_type)) - } else { - None - }; - let mut conn = dst_mbt.open_or_new().await?; let is_empty_db = is_empty_database(&mut conn).await?; src_mbt.attach_to(&mut conn, "sourceDb").await?; - let dst_type; - if let Some((dif_mbt, dif_type)) = &dif { + let dst_type: MbtType; + if let Some((dif_mbt, dif_type, _)) = &dif { if !is_empty_db { return Err(MbtError::NonEmptyTargetFile(self.options.dst_file)); } - dst_type = self.options.dst_type.unwrap_or(src_type); + dst_type = self.options.dst_type().unwrap_or(src_type); dif_mbt.attach_to(&mut conn, "diffDb").await?; let dif_path = dif_mbt.filepath(); - info!("Comparing {src_mbt} ({src_type}) and {dif_path} ({dif_type}) into a new file {dst_mbt} ({dst_type})"); + if self.options.diff_with_file.is_some() { + info!("Comparing {src_mbt} ({src_type}) and {dif_path} ({dif_type}) into a new file {dst_mbt} ({dst_type})"); + } else { + info!("Applying patch from {dif_path} ({dif_type}) to {src_mbt} ({src_type}) into a new file {dst_mbt} ({dst_type})"); + } } else if is_empty_db { - dst_type = self.options.dst_type.unwrap_or(src_type); + dst_type = self.options.dst_type().unwrap_or(src_type); info!("Copying {src_mbt} ({src_type}) to a new file {dst_mbt} ({dst_type})"); } else { - dst_type = dst_mbt.detect_type(&mut conn).await?; + dst_type = self.validate_dst_type(dst_mbt.detect_type(&mut conn).await?)?; info!("Copying {src_mbt} ({src_type}) to an existing file {dst_mbt} ({dst_type})"); } @@ -176,8 +210,12 @@ impl MbtileCopierInt { self.init_new_schema(&mut conn, src_type, dst_type).await?; } - let select_from = if let Some((_, dif_type)) = &dif { - Self::get_select_from_with_diff(*dif_type, dst_type) + let select_from = if let Some((_, dif_type, _)) = &dif { + if self.options.diff_with_file.is_some() { + Self::get_select_from_with_diff(*dif_type, dst_type) + } else { + Self::get_select_from_apply_patch(src_type, *dif_type, dst_type) + } } else { Self::get_select_from(src_type, dst_type).to_string() }; @@ -215,12 +253,12 @@ impl MbtileCopierInt { debug!("Copying to {dst_type} with {sql} {query_args:?}"); rusqlite_conn.execute(&sql, params_from_iter(query_args))? } - Normalized => { + Normalized { .. } => { let sql = format!( " INSERT OR IGNORE INTO images (tile_id, tile_data) - SELECT hash as tile_id, tile_data + SELECT tile_hash as tile_id, tile_data FROM ({select_from})" ); debug!("Copying to {dst_type} with {sql} {query_args:?}"); @@ -230,7 +268,7 @@ impl MbtileCopierInt { " INSERT {on_dupl} INTO map (zoom_level, tile_column, tile_row, tile_id) - SELECT zoom_level, tile_column, tile_row, hash as tile_id + SELECT zoom_level, tile_column, tile_row, tile_hash as tile_id FROM ({select_from} {sql_cond})" ); debug!("Copying to {dst_type} with {sql} {query_args:?}"); @@ -238,30 +276,54 @@ impl MbtileCopierInt { } }; - let sql = if self.options.diff_with_file.is_some() { - debug!("Copying metadata with 'INSERT {on_dupl}', taking into account diff file"); + let sql; + if dif.is_some() { // Insert all rows from diffDb.metadata if they do not exist or are different in sourceDb.metadata. // Also insert all names from sourceDb.metadata that do not exist in diffDb.metadata, with their value set to NULL. // Rename agg_tiles_hash to agg_tiles_hash_in_diff because agg_tiles_hash will be auto-added later - format!( + if self.options.diff_with_file.is_some() { + sql = format!( " INSERT {on_dupl} INTO metadata (name, value) - SELECT IIF(dif.name = '{AGG_TILES_HASH}','{AGG_TILES_HASH_IN_DIFF}', dif.name) as name, - dif.value as value - FROM diffDb.metadata AS dif LEFT JOIN sourceDb.metadata AS src - ON dif.name = src.name - WHERE (dif.value != src.value OR src.value ISNULL) - AND dif.name != '{AGG_TILES_HASH_IN_DIFF}' - UNION ALL - SELECT src.name as name, NULL as value - FROM sourceDb.metadata AS src LEFT JOIN diffDb.metadata AS dif - ON src.name = dif.name - WHERE dif.value ISNULL AND src.name NOT IN ('{AGG_TILES_HASH}', '{AGG_TILES_HASH_IN_DIFF}');" - ) + SELECT IIF(name = '{AGG_TILES_HASH}','{AGG_TILES_HASH_IN_DIFF}', name) as name + , value + FROM ( + SELECT COALESCE(difMD.name, srcMD.name) as name + , difMD.value as value + FROM sourceDb.metadata AS srcMD FULL JOIN diffDb.metadata AS difMD + ON srcMD.name = difMD.name + WHERE srcMD.value != difMD.value OR srcMD.value ISNULL OR difMD.value ISNULL + ) joinedMD + WHERE name != '{AGG_TILES_HASH_IN_DIFF}'" + ); + } else { + sql = format!( + " + INSERT {on_dupl} INTO metadata (name, value) + SELECT IIF(name = '{AGG_TILES_HASH_IN_DIFF}','{AGG_TILES_HASH}', name) as name + , value + FROM ( + SELECT COALESCE(srcMD.name, difMD.name) as name + , COALESCE(difMD.value, srcMD.value) as value + FROM sourceDb.metadata AS srcMD FULL JOIN diffDb.metadata AS difMD + ON srcMD.name = difMD.name + WHERE difMD.name ISNULL OR difMD.value NOTNULL + ) joinedMD + WHERE name != '{AGG_TILES_HASH}'" + ); + } + if self.options.diff_with_file.is_some() { + debug!("Copying metadata, taking into account diff file with {sql}"); + } else { + debug!("Copying metadata, and applying the diff file with {sql}"); + } } else { - debug!("Copying metadata with 'INSERT {on_dupl}'"); - format!("INSERT {on_dupl} INTO metadata SELECT name, value FROM sourceDb.metadata") - }; + sql = format!( + " + INSERT {on_dupl} INTO metadata SELECT name, value FROM sourceDb.metadata" + ); + debug!("Copying metadata with {sql}"); + } rusqlite_conn.execute(&sql, [])?; // SAFETY: must drop rusqlite_conn before handle_lock, or place the code since lock in a separate scope @@ -279,6 +341,25 @@ impl MbtileCopierInt { Ok(conn) } + /// Check if the detected destination file type matches the one given by the options + fn validate_dst_type(&self, dst_type: MbtType) -> MbtResult { + if let Some(cli) = self.options.dst_type() { + match (cli, dst_type) { + (Flat, Flat) + | (FlatWithHash, FlatWithHash) + | (Normalized { .. }, Normalized { .. }) => {} + (cli, dst) => { + return Err(MbtError::MismatchedTargetType( + self.options.dst_file.to_path_buf(), + dst, + cli, + )) + } + } + } + Ok(dst_type) + } + async fn init_new_schema( &self, conn: &mut SqliteConnection, @@ -317,11 +398,11 @@ impl MbtileCopierInt { match dst { Flat => create_flat_tables(&mut *conn).await?, FlatWithHash => create_flat_with_hash_tables(&mut *conn).await?, - Normalized => create_normalized_tables(&mut *conn).await?, + Normalized { .. } => create_normalized_tables(&mut *conn).await?, }; }; - if dst == Normalized { + if dst.is_normalized() { // Some normalized mbtiles files might not have this view, so even if src == dst, it might not exist create_tiles_with_hash_view(&mut *conn).await?; } @@ -338,7 +419,7 @@ impl MbtileCopierInt { let (main_table, tile_identifier) = match dst_type { Flat => ("tiles", "tile_data"), FlatWithHash => ("tiles_with_hash", "tile_data"), - Normalized => ("map", "tile_id"), + Normalized { .. } => ("map", "tile_id"), }; format!( @@ -356,24 +437,111 @@ impl MbtileCopierInt { } } + fn get_select_from_apply_patch( + src_type: MbtType, + dif_type: MbtType, + dst_type: MbtType, + ) -> String { + fn query_for_dst(frm_db: &'static str, frm_type: MbtType, to_type: MbtType) -> String { + match to_type { + Flat => format!("{frm_db}.tiles"), + FlatWithHash => match frm_type { + Flat => format!( + " + (SELECT zoom_level, tile_column, tile_row, tile_data, md5_hex(tile_data) AS tile_hash + FROM {frm_db}.tiles)" + ), + FlatWithHash => format!("{frm_db}.tiles_with_hash"), + Normalized { hash_view } => { + if hash_view { + format!("{frm_db}.tiles_with_hash") + } else { + format!( + " + (SELECT zoom_level, tile_column, tile_row, tile_data, map.tile_id AS tile_hash + FROM {frm_db}.map JOIN {frm_db}.images ON map.tile_id = images.tile_id)" + ) + } + } + }, + Normalized { .. } => match frm_type { + Flat => format!( + " + (SELECT zoom_level, tile_column, tile_row, tile_data, md5_hex(tile_data) AS tile_hash + FROM {frm_db}.tiles)" + ), + FlatWithHash => format!("{frm_db}.tiles_with_hash"), + Normalized { hash_view } => { + if hash_view { + format!("{frm_db}.tiles_with_hash") + } else { + format!( + " + (SELECT zoom_level, tile_column, tile_row, tile_data, map.tile_id AS tile_hash + FROM {frm_db}.map JOIN {frm_db}.images ON map.tile_id = images.tile_id)" + ) + } + } + }, + } + } + + let tile_hash_expr = if dst_type == Flat { + String::new() + } else { + fn get_tile_hash_expr(tbl: &str, typ: MbtType) -> String { + match typ { + Flat => format!("IIF({tbl}.tile_data ISNULL, NULL, md5_hex({tbl}.tile_data))"), + FlatWithHash => format!("{tbl}.tile_hash"), + Normalized { .. } => format!("{tbl}.tile_hash"), + } + } + + format!( + ", COALESCE({}, {}) as tile_hash", + get_tile_hash_expr("difTiles", dif_type), + get_tile_hash_expr("srcTiles", src_type) + ) + }; + + let src_tiles = query_for_dst("sourceDb", src_type, dst_type); + let diff_tiles = query_for_dst("diffDb", dif_type, dst_type); + + // Take dif tile_data if it is set, otherwise take the one from src + // Skip tiles if src and dif both have a matching index, but the dif tile_data is NULL + format!( + " + SELECT COALESCE(srcTiles.zoom_level, difTiles.zoom_level) as zoom_level + , COALESCE(srcTiles.tile_column, difTiles.tile_column) as tile_column + , COALESCE(srcTiles.tile_row, difTiles.tile_row) as tile_row + , COALESCE(difTiles.tile_data, srcTiles.tile_data) as tile_data + {tile_hash_expr} + FROM {src_tiles} AS srcTiles FULL JOIN {diff_tiles} AS difTiles + ON srcTiles.zoom_level = difTiles.zoom_level + AND srcTiles.tile_column = difTiles.tile_column + AND srcTiles.tile_row = difTiles.tile_row + WHERE (difTiles.zoom_level ISNULL OR difTiles.tile_data NOTNULL)" + ) + } + fn get_select_from_with_diff(dif_type: MbtType, dst_type: MbtType) -> String { - let hash_col_sql; + let tile_hash_expr; let diff_tiles; if dst_type == Flat { - hash_col_sql = ""; + tile_hash_expr = ""; diff_tiles = "diffDb.tiles"; } else { - hash_col_sql = match dif_type { - Flat => ", COALESCE(md5_hex(difTiles.tile_data), '') as hash", - FlatWithHash => ", COALESCE(difTiles.tile_hash, '') as hash", - Normalized => ", COALESCE(difTiles.hash, '') as hash", + tile_hash_expr = match dif_type { + Flat => ", COALESCE(md5_hex(difTiles.tile_data), '') as tile_hash", + FlatWithHash => ", COALESCE(difTiles.tile_hash, '') as tile_hash", + Normalized { .. } => ", COALESCE(difTiles.tile_hash, '') as tile_hash", }; diff_tiles = match dif_type { Flat => "diffDb.tiles", FlatWithHash => "diffDb.tiles_with_hash", - Normalized => { + Normalized { .. } => { " - (SELECT zoom_level, tile_column, tile_row, tile_data, map.tile_id AS hash + (SELECT zoom_level, tile_column, tile_row, tile_data, map.tile_id AS tile_hash FROM diffDb.map JOIN diffDb.images ON diffDb.map.tile_id = diffDb.images.tile_id)" } }; @@ -385,7 +553,7 @@ impl MbtileCopierInt { , COALESCE(srcTiles.tile_column, difTiles.tile_column) as tile_column , COALESCE(srcTiles.tile_row, difTiles.tile_row) as tile_row , difTiles.tile_data as tile_data - {hash_col_sql} + {tile_hash_expr} FROM sourceDb.tiles AS srcTiles FULL JOIN {diff_tiles} AS difTiles ON srcTiles.zoom_level = difTiles.zoom_level AND srcTiles.tile_column = difTiles.tile_column @@ -403,19 +571,19 @@ impl MbtileCopierInt { match src_type { Flat => { " - SELECT zoom_level, tile_column, tile_row, tile_data, md5_hex(tile_data) as hash + SELECT zoom_level, tile_column, tile_row, tile_data, md5_hex(tile_data) as tile_hash FROM sourceDb.tiles WHERE TRUE" } FlatWithHash => { " - SELECT zoom_level, tile_column, tile_row, tile_data, tile_hash AS hash + SELECT zoom_level, tile_column, tile_row, tile_data, tile_hash FROM sourceDb.tiles_with_hash WHERE TRUE" } - Normalized => { + Normalized { .. } => { " - SELECT zoom_level, tile_column, tile_row, tile_data, map.tile_id AS hash + SELECT zoom_level, tile_column, tile_row, tile_data, map.tile_id AS tile_hash FROM sourceDb.map JOIN sourceDb.images ON sourceDb.map.tile_id = sourceDb.images.tile_id WHERE TRUE" @@ -461,6 +629,11 @@ mod tests { use super::*; + const FLAT: Option = Some(MbtTypeCli::Flat); + const FLAT_WITH_HASH: Option = Some(MbtTypeCli::FlatWithHash); + const NORM_CLI: Option = Some(MbtTypeCli::Normalized); + const NORM_WITH_VIEW: MbtType = Normalized { hash_view: true }; + async fn get_one(conn: &mut SqliteConnection, sql: &str) -> T where for<'r> T: Decode<'r, Sqlite> + Type, @@ -471,11 +644,11 @@ mod tests { async fn verify_copy_all( src_filepath: PathBuf, dst_filepath: PathBuf, - dst_type: Option, + dst_type_cli: Option, expected_dst_type: MbtType, ) -> MbtResult<()> { let mut opt = MbtilesCopier::new(src_filepath.clone(), dst_filepath.clone()); - opt.dst_type = dst_type; + opt.dst_type_cli = dst_type_cli; let mut dst_conn = opt.run().await?; Mbtiles::new(src_filepath)? @@ -528,7 +701,7 @@ mod tests { let dst = PathBuf::from( "file:copy_flat_from_flat_with_hash_tables_mem_db?mode=memory&cache=shared", ); - verify_copy_all(src, dst, Some(Flat), Flat).await + verify_copy_all(src, dst, FLAT, Flat).await } #[actix_rt::test] @@ -536,7 +709,7 @@ mod tests { let src = PathBuf::from("../tests/fixtures/mbtiles/geography-class-png.mbtiles"); let dst = PathBuf::from("file:copy_flat_from_normalized_tables_mem_db?mode=memory&cache=shared"); - verify_copy_all(src, dst, Some(Flat), Flat).await + verify_copy_all(src, dst, FLAT, Flat).await } #[actix_rt::test] @@ -552,7 +725,7 @@ mod tests { let dst = PathBuf::from( "file:copy_flat_with_hash_from_flat_tables_mem_db?mode=memory&cache=shared", ); - verify_copy_all(src, dst, Some(FlatWithHash), FlatWithHash).await + verify_copy_all(src, dst, FLAT_WITH_HASH, FlatWithHash).await } #[actix_rt::test] @@ -561,14 +734,14 @@ mod tests { let dst = PathBuf::from( "file:copy_flat_with_hash_from_normalized_tables_mem_db?mode=memory&cache=shared", ); - verify_copy_all(src, dst, Some(FlatWithHash), FlatWithHash).await + verify_copy_all(src, dst, FLAT_WITH_HASH, FlatWithHash).await } #[actix_rt::test] async fn copy_normalized_tables() -> MbtResult<()> { let src = PathBuf::from("../tests/fixtures/mbtiles/geography-class-png.mbtiles"); let dst = PathBuf::from("file:copy_normalized_tables_mem_db?mode=memory&cache=shared"); - verify_copy_all(src, dst, None, Normalized).await + verify_copy_all(src, dst, None, NORM_WITH_VIEW).await } #[actix_rt::test] @@ -576,7 +749,7 @@ mod tests { let src = PathBuf::from("../tests/fixtures/mbtiles/world_cities.mbtiles"); let dst = PathBuf::from("file:copy_normalized_from_flat_tables_mem_db?mode=memory&cache=shared"); - verify_copy_all(src, dst, Some(Normalized), Normalized).await + verify_copy_all(src, dst, NORM_CLI, NORM_WITH_VIEW).await } #[actix_rt::test] @@ -585,7 +758,7 @@ mod tests { let dst = PathBuf::from( "file:copy_normalized_from_flat_with_hash_tables_mem_db?mode=memory&cache=shared", ); - verify_copy_all(src, dst, Some(Normalized), Normalized).await + verify_copy_all(src, dst, NORM_CLI, NORM_WITH_VIEW).await } #[actix_rt::test] @@ -655,23 +828,6 @@ mod tests { Ok(()) } - #[actix_rt::test] - async fn ignore_dst_type_when_copy_to_existing() -> MbtResult<()> { - let src_file = PathBuf::from("../tests/fixtures/mbtiles/world_cities_modified.mbtiles"); - - // Copy the dst file to an in-memory DB - let dst_file = PathBuf::from("../tests/fixtures/mbtiles/world_cities.mbtiles"); - let dst = PathBuf::from( - "file:ignore_dst_type_when_copy_to_existing_mem_db?mode=memory&cache=shared", - ); - - let _dst_conn = MbtilesCopier::new(dst_file.clone(), dst.clone()) - .run() - .await?; - - verify_copy_all(src_file, dst, Some(Normalized), Flat).await - } - #[actix_rt::test] async fn copy_to_existing_abort_mode() { let src = PathBuf::from("../tests/fixtures/mbtiles/world_cities_modified.mbtiles"); diff --git a/martin-mbtiles/src/errors.rs b/martin-mbtiles/src/errors.rs index f8b8b14ab..cb732b66e 100644 --- a/martin-mbtiles/src/errors.rs +++ b/martin-mbtiles/src/errors.rs @@ -3,6 +3,8 @@ use std::path::PathBuf; use martin_tile_utils::TileInfo; use sqlite_hashes::rusqlite; +use crate::MbtType; + #[derive(thiserror::Error, Debug)] pub enum MbtError { #[error("The source and destination MBTiles files are the same: {}", .0.display())] @@ -55,6 +57,12 @@ pub enum MbtError { #[error("Unexpected duplicate tiles found when copying")] DuplicateValues, + + #[error("Applying a patch while diffing is not supported")] + CannotApplyPatchAndDiff, + + #[error("The MBTiles file {0} has data of type {1}, but the desired type was set to {2}")] + MismatchedTargetType(PathBuf, MbtType, MbtType), } pub type MbtResult = Result; diff --git a/martin-mbtiles/src/lib.rs b/martin-mbtiles/src/lib.rs index d31dcb7af..434fb6a92 100644 --- a/martin-mbtiles/src/lib.rs +++ b/martin-mbtiles/src/lib.rs @@ -5,8 +5,8 @@ pub use errors::{MbtError, MbtResult}; mod mbtiles; pub use mbtiles::{ - calc_agg_tiles_hash, IntegrityCheckType, MbtType, Mbtiles, Metadata, AGG_TILES_HASH, - AGG_TILES_HASH_IN_DIFF, + calc_agg_tiles_hash, IntegrityCheckType, MbtType, MbtTypeCli, Mbtiles, Metadata, + AGG_TILES_HASH, AGG_TILES_HASH_IN_DIFF, }; mod pool; diff --git a/martin-mbtiles/src/mbtiles.rs b/martin-mbtiles/src/mbtiles.rs index faf37a043..2faf2b894 100644 --- a/martin-mbtiles/src/mbtiles.rs +++ b/martin-mbtiles/src/mbtiles.rs @@ -22,7 +22,8 @@ use tilejson::{tilejson, Bounds, Center, TileJSON}; use crate::errors::{MbtError, MbtResult}; use crate::queries::{ - is_flat_tables_type, is_flat_with_hash_tables_type, is_normalized_tables_type, + has_tiles_with_hash, is_flat_tables_type, is_flat_with_hash_tables_type, + is_normalized_tables_type, }; use crate::MbtError::{ AggHashMismatch, AggHashValueNotFound, FailedIntegrityCheck, IncorrectTileHash, @@ -65,12 +66,30 @@ pub const AGG_TILES_HASH_IN_DIFF: &str = "agg_tiles_hash_after_apply"; #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, EnumDisplay)] #[enum_display(case = "Kebab")] #[cfg_attr(feature = "cli", derive(ValueEnum))] -pub enum MbtType { +pub enum MbtTypeCli { Flat, FlatWithHash, Normalized, } +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, EnumDisplay)] +#[enum_display(case = "Kebab")] +pub enum MbtType { + Flat, + FlatWithHash, + Normalized { hash_view: bool }, +} + +impl MbtType { + pub fn is_normalized(&self) -> bool { + matches!(self, Self::Normalized { .. }) + } + + pub fn is_normalized_with_view(&self) -> bool { + matches!(self, Self::Normalized { hash_view: true }) + } +} + #[derive(PartialEq, Eq, Default, Debug, Clone, EnumDisplay)] #[enum_display(case = "Kebab")] #[cfg_attr(feature = "cli", derive(ValueEnum))] @@ -437,8 +456,10 @@ impl Mbtiles { for<'e> &'e mut T: SqliteExecutor<'e>, { debug!("Detecting MBTiles type for {self}"); - let mbt_type = if is_normalized_tables_type(&mut *conn).await? { - MbtType::Normalized + let typ = if is_normalized_tables_type(&mut *conn).await? { + MbtType::Normalized { + hash_view: has_tiles_with_hash(&mut *conn).await?, + } } else if is_flat_with_hash_tables_type(&mut *conn).await? { MbtType::FlatWithHash } else if is_flat_tables_type(&mut *conn).await? { @@ -447,10 +468,10 @@ impl Mbtiles { return Err(MbtError::InvalidDataFormat(self.filepath.clone())); }; - self.check_for_uniqueness_constraint(&mut *conn, mbt_type) + self.check_for_uniqueness_constraint(&mut *conn, typ) .await?; - Ok(mbt_type) + Ok(typ) } async fn check_for_uniqueness_constraint( @@ -464,7 +485,7 @@ impl Mbtiles { let table_name = match mbt_type { MbtType::Flat => "tiles", MbtType::FlatWithHash => "tiles_with_hash", - MbtType::Normalized => "map", + MbtType::Normalized { .. } => "map", }; let indexes = query("SELECT name FROM pragma_index_list(?) WHERE [unique] = 1") @@ -596,7 +617,7 @@ impl Mbtiles { WHERE expected != computed LIMIT 1;" } - MbtType::Normalized => { + MbtType::Normalized { .. } => { "SELECT expected, computed FROM ( SELECT upper(tile_id) AS expected, @@ -803,7 +824,7 @@ mod tests { let (mut conn, mbt) = open("../tests/fixtures/mbtiles/geography-class-jpg.mbtiles").await?; let res = mbt.detect_type(&mut conn).await?; - assert_eq!(res, MbtType::Normalized); + assert_eq!(res, MbtType::Normalized { hash_view: false }); let (mut conn, mbt) = open(":memory:").await?; let res = mbt.detect_type(&mut conn).await; diff --git a/martin-mbtiles/src/patcher.rs b/martin-mbtiles/src/patcher.rs index d56cb5cfd..94444a917 100644 --- a/martin-mbtiles/src/patcher.rs +++ b/martin-mbtiles/src/patcher.rs @@ -31,7 +31,7 @@ pub async fn apply_patch(src_file: PathBuf, patch_file: PathBuf) -> MbtResult<() SELECT zoom_level, tile_column, tile_row, tile_data, tile_hash AS hash FROM patchDb.tiles_with_hash" } - Normalized => { + Normalized { .. } => { " SELECT zoom_level, tile_column, tile_row, tile_data, map.tile_id AS hash FROM patchDb.map LEFT JOIN patchDb.images @@ -58,7 +58,7 @@ pub async fn apply_patch(src_file: PathBuf, patch_file: PathBuf) -> MbtResult<() {select_from}" )], ), - Normalized => ( + Normalized { .. } => ( "map", vec![ format!( @@ -93,7 +93,7 @@ pub async fn apply_patch(src_file: PathBuf, patch_file: PathBuf) -> MbtResult<() .execute(&mut conn) .await?; - if src_type == Normalized { + if src_type.is_normalized() { debug!("Removing unused tiles from the images table (normalized schema)"); query("DELETE FROM images WHERE tile_id NOT IN (SELECT tile_id FROM map)") .execute(&mut conn) diff --git a/martin-mbtiles/tests/mbtiles.rs b/martin-mbtiles/tests/mbtiles.rs index 833452b9e..2e7ec641b 100644 --- a/martin-mbtiles/tests/mbtiles.rs +++ b/martin-mbtiles/tests/mbtiles.rs @@ -6,8 +6,10 @@ use ctor::ctor; use insta::{allow_duplicates, assert_display_snapshot}; use log::info; use martin_mbtiles::IntegrityCheckType::Off; -use martin_mbtiles::MbtType::{Flat, FlatWithHash, Normalized}; -use martin_mbtiles::{apply_patch, create_flat_tables, MbtResult, MbtType, Mbtiles, MbtilesCopier}; +use martin_mbtiles::MbtTypeCli::{Flat, FlatWithHash, Normalized}; +use martin_mbtiles::{ + apply_patch, create_flat_tables, MbtResult, MbtTypeCli, Mbtiles, MbtilesCopier, +}; use pretty_assertions::assert_eq as pretty_assert_eq; use rstest::{fixture, rstest}; use serde::Serialize; @@ -84,7 +86,7 @@ fn copier(src: &Mbtiles, dst: &Mbtiles) -> MbtilesCopier { MbtilesCopier::new(path(src), path(dst)) } -fn shorten(v: MbtType) -> &'static str { +fn shorten(v: MbtTypeCli) -> &'static str { match v { Flat => "flat", FlatWithHash => "hash", @@ -99,7 +101,7 @@ async fn open(file: &str) -> MbtResult<(Mbtiles, SqliteConnection)> { } macro_rules! open { - ($function:tt, $($arg:tt)*) => { + ($function:ident, $($arg:tt)*) => { open!(@"", $function, $($arg)*) }; (@$extra:literal, $function:tt, $($arg:tt)*) => {{ @@ -110,18 +112,18 @@ macro_rules! open { /// Create a new SQLite file of given type without agg_tiles_hash metadata value macro_rules! new_file_no_hash { - ($function:tt, $dst_type:expr, $sql_meta:expr, $sql_data:expr, $($arg:tt)*) => {{ - new_file!(@true, $function, $dst_type, $sql_meta, $sql_data, $($arg)*) + ($function:ident, $dst_type_cli:expr, $sql_meta:expr, $sql_data:expr, $($arg:tt)*) => {{ + new_file!(@true, $function, $dst_type_cli, $sql_meta, $sql_data, $($arg)*) }}; } -/// Create a new SQLite file of type $dst_type with the given metadata and tiles +/// Create a new SQLite file of type $dst_type_cli with the given metadata and tiles macro_rules! new_file { - ($function:tt, $dst_type:expr, $sql_meta:expr, $sql_data:expr, $($arg:tt)*) => { - new_file!(@false, $function, $dst_type, $sql_meta, $sql_data, $($arg)*) + ($function:ident, $dst_type_cli:expr, $sql_meta:expr, $sql_data:expr, $($arg:tt)*) => { + new_file!(@false, $function, $dst_type_cli, $sql_meta, $sql_data, $($arg)*) }; - (@$skip_agg:expr, $function:tt, $dst_type:expr, $sql_meta:expr, $sql_data:expr, $($arg:tt)*) => {{ + (@$skip_agg:expr, $function:tt, $dst_type_cli:expr, $sql_meta:expr, $sql_data:expr, $($arg:tt)*) => {{ let (tmp_mbt, mut cn_tmp) = open!(@"temp", $function, $($arg)*); create_flat_tables(&mut cn_tmp).await.unwrap(); cn_tmp.execute($sql_data).await.unwrap(); @@ -129,7 +131,7 @@ macro_rules! new_file { let (dst_mbt, cn_dst) = open!($function, $($arg)*); let mut opt = copier(&tmp_mbt, &dst_mbt); - opt.dst_type = Some($dst_type); + opt.dst_type_cli = Some($dst_type_cli); opt.skip_agg_tiles_hash = $skip_agg; opt.run().await.unwrap(); @@ -146,61 +148,84 @@ macro_rules! assert_snapshot { }}; } -macro_rules! assert_dump { - ($connection:expr, $($arg:tt)*) => {{ - let dmp = dump($connection).await.unwrap(); - assert_snapshot!(&dmp, $($arg)*); - dmp - }}; +#[derive(Default)] +struct Databases( + HashMap<(&'static str, MbtTypeCli), (Vec, Mbtiles, SqliteConnection)>, +); + +impl Databases { + fn add( + &mut self, + name: &'static str, + typ: MbtTypeCli, + dump: Vec, + mbtiles: Mbtiles, + conn: SqliteConnection, + ) { + self.0.insert((name, typ), (dump, mbtiles, conn)); + } + fn dump(&self, name: &'static str, typ: MbtTypeCli) -> &Vec { + &self.0.get(&(name, typ)).unwrap().0 + } + fn mbtiles(&self, name: &'static str, typ: MbtTypeCli) -> &Mbtiles { + &self.0.get(&(name, typ)).unwrap().1 + } } -type Databases = HashMap<(&'static str, MbtType), Vec>; - +/// Generate a set of databases for testing, and validate them against snapshot files. +/// These dbs will be used by other tests to check against in various conditions. #[fixture] #[once] fn databases() -> Databases { futures::executor::block_on(async { - let mut result = HashMap::new(); + let mut result = Databases::default(); for &mbt_typ in &[Flat, FlatWithHash, Normalized] { let typ = shorten(mbt_typ); - let (raw_mbt, mut cn) = new_file_no_hash!( + let (raw_mbt, mut raw_cn) = new_file_no_hash!( databases, mbt_typ, METADATA_V1, TILES_V1, "{typ}__v1-no-hash" ); - let dmp = assert_dump!(&mut cn, "{typ}__v1-no-hash"); - result.insert(("v1_no_hash", mbt_typ), dmp); + let dmp = dump(&mut raw_cn).await.unwrap(); + assert_snapshot!(&dmp, "{typ}__v1-no-hash"); + result.add("v1_no_hash", mbt_typ, dmp, raw_mbt, raw_cn); let (v1_mbt, mut v1_cn) = open!(databases, "{typ}__v1"); - copier(&raw_mbt, &v1_mbt).run().await.unwrap(); - let dmp = assert_dump!(&mut v1_cn, "{typ}__v1"); + let raw_mbt = result.mbtiles("v1_no_hash", mbt_typ); + copier(raw_mbt, &v1_mbt).run().await.unwrap(); + let dmp = dump(&mut v1_cn).await.unwrap(); + assert_snapshot!(&dmp, "{typ}__v1"); let hash = v1_mbt.validate(Off, false).await.unwrap(); allow_duplicates! { assert_display_snapshot!(hash, @"096A8399D486CF443A5DF0CEC1AD8BB2"); } - result.insert(("v1", mbt_typ), dmp); + result.add("v1", mbt_typ, dmp, v1_mbt, v1_cn); let (v2_mbt, mut v2_cn) = new_file!(databases, mbt_typ, METADATA_V2, TILES_V2, "{typ}__v2"); - let dmp = assert_dump!(&mut v2_cn, "{typ}__v2"); + let dmp = dump(&mut v2_cn).await.unwrap(); + assert_snapshot!(&dmp, "{typ}__v2"); let hash = v2_mbt.validate(Off, false).await.unwrap(); allow_duplicates! { assert_display_snapshot!(hash, @"FE0D3090E8B4E89F2C755C08E8D76BEA"); } - result.insert(("v2", mbt_typ), dmp); + result.add("v2", mbt_typ, dmp, v2_mbt, v2_cn); let (dif_mbt, mut dif_cn) = open!(databases, "{typ}__dif"); - let mut opt = copier(&v1_mbt, &dif_mbt); - opt.diff_with_file = Some(path(&v2_mbt)); + let v1_mbt = result.mbtiles("v1", mbt_typ); + let mut opt = copier(v1_mbt, &dif_mbt); + let v2_mbt = result.mbtiles("v2", mbt_typ); + opt.diff_with_file = Some(path(v2_mbt)); opt.run().await.unwrap(); - let dmp = assert_dump!(&mut dif_cn, "{typ}__dif"); + let dmp = dump(&mut dif_cn).await.unwrap(); + assert_snapshot!(&dmp, "{typ}__dif"); let hash = dif_mbt.validate(Off, false).await.unwrap(); allow_duplicates! { assert_display_snapshot!(hash, @"B86122579EDCDD4C51F3910894FCC1A1"); } - result.insert(("dif", mbt_typ), dmp); + result.add("dif", mbt_typ, dmp, dif_mbt, dif_cn); } result }) @@ -210,8 +235,8 @@ fn databases() -> Databases { #[trace] #[actix_rt::test] async fn convert( - #[values(Flat, FlatWithHash, Normalized)] frm_type: MbtType, - #[values(Flat, FlatWithHash, Normalized)] dst_type: MbtType, + #[values(Flat, FlatWithHash, Normalized)] frm_type: MbtTypeCli, + #[values(Flat, FlatWithHash, Normalized)] dst_type: MbtTypeCli, #[notrace] databases: &Databases, ) -> MbtResult<()> { let (frm, to) = (shorten(frm_type), shorten(dst_type)); @@ -219,23 +244,23 @@ async fn convert( let (frm_mbt, _frm_cn) = new_file!(convert, frm_type, METADATA_V1, TILES_V1, "{frm}-{to}"); let mut opt = copier(&frm_mbt, &mem); - opt.dst_type = Some(dst_type); + opt.dst_type_cli = Some(dst_type); let dmp = dump(&mut opt.run().await?).await?; - pretty_assert_eq!(databases.get(&("v1", dst_type)).unwrap(), &dmp); + pretty_assert_eq!(databases.dump("v1", dst_type), &dmp); let mut opt = copier(&frm_mbt, &mem); - opt.dst_type = Some(dst_type); + opt.dst_type_cli = Some(dst_type); opt.zoom_levels.insert(6); let z6only = dump(&mut opt.run().await?).await?; assert_snapshot!(z6only, "v1__z6__{frm}-{to}"); let mut opt = copier(&frm_mbt, &mem); - opt.dst_type = Some(dst_type); + opt.dst_type_cli = Some(dst_type); opt.min_zoom = Some(6); pretty_assert_eq!(&z6only, &dump(&mut opt.run().await?).await?); let mut opt = copier(&frm_mbt, &mem); - opt.dst_type = Some(dst_type); + opt.dst_type_cli = Some(dst_type); opt.min_zoom = Some(6); opt.max_zoom = Some(6); pretty_assert_eq!(&z6only, &dump(&mut opt.run().await?).await?); @@ -246,42 +271,45 @@ async fn convert( #[rstest] #[trace] #[actix_rt::test] -async fn diff_apply( - #[values(Flat, FlatWithHash, Normalized)] v1_type: MbtType, - #[values(Flat, FlatWithHash, Normalized)] v2_type: MbtType, - #[values(None, Some(Flat), Some(FlatWithHash), Some(Normalized))] dif_type: Option, +async fn diff_and_patch( + #[values(Flat, FlatWithHash, Normalized)] v1_type: MbtTypeCli, + #[values(Flat, FlatWithHash, Normalized)] v2_type: MbtTypeCli, + #[values(None, Some(Flat), Some(FlatWithHash), Some(Normalized))] dif_type: Option, #[notrace] databases: &Databases, ) -> MbtResult<()> { let (v1, v2) = (shorten(v1_type), shorten(v2_type)); let dif = dif_type.map(shorten).unwrap_or("dflt"); let prefix = format!("{v2}-{v1}={dif}"); - let (v1_mbt, _v1_cn) = new_file! {diff_apply, v1_type, METADATA_V1, TILES_V1, "{prefix}__v1"}; - let (v2_mbt, _v2_cn) = new_file! {diff_apply, v2_type, METADATA_V2, TILES_V2, "{prefix}__v2"}; - let (dif_mbt, mut dif_cn) = open!(diff_apply, "{prefix}__dif"); + let v1_mbt = databases.mbtiles("v1", v1_type); + let v2_mbt = databases.mbtiles("v2", v2_type); + let (dif_mbt, mut dif_cn) = open!(diff_and_patchdiff_and_patch, "{prefix}__dif"); info!("TEST: Compare v1 with v2, and copy anything that's different (i.e. mathematically: v2-v1=diff)"); - let mut opt = copier(&v1_mbt, &dif_mbt); - opt.diff_with_file = Some(path(&v2_mbt)); + let mut opt = copier(v1_mbt, &dif_mbt); + opt.diff_with_file = Some(path(v2_mbt)); if let Some(dif_type) = dif_type { - opt.dst_type = Some(dif_type); + opt.dst_type_cli = Some(dif_type); } opt.run().await?; pretty_assert_eq!( &dump(&mut dif_cn).await?, - databases - .get(&("dif", dif_type.unwrap_or(v1_type))) - .unwrap() + databases.dump("dif", dif_type.unwrap_or(v1_type)) ); for target_type in &[Flat, FlatWithHash, Normalized] { let trg = shorten(*target_type); let prefix = format!("{prefix}__to__{trg}"); - let expected_v2 = databases.get(&("v2", *target_type)).unwrap(); + let expected_v2 = databases.dump("v2", *target_type); info!("TEST: Applying the difference (v2-v1=diff) to v1, should get v2"); - let (tar1_mbt, mut tar1_cn) = - new_file! {diff_apply, *target_type, METADATA_V1, TILES_V1, "{prefix}__v1"}; + let (tar1_mbt, mut tar1_cn) = new_file!( + diff_and_patch, + *target_type, + METADATA_V1, + TILES_V1, + "{prefix}__v1" + ); apply_patch(path(&tar1_mbt), path(&dif_mbt)).await?; let hash_v1 = tar1_mbt.validate(Off, false).await?; allow_duplicates! { @@ -292,7 +320,7 @@ async fn diff_apply( info!("TEST: Applying the difference (v2-v1=diff) to v2, should not modify it"); let (tar2_mbt, mut tar2_cn) = - new_file! {diff_apply, *target_type, METADATA_V2, TILES_V2, "{prefix}__v2"}; + new_file! {diff_and_patch, *target_type, METADATA_V2, TILES_V2, "{prefix}__v2"}; apply_patch(path(&tar2_mbt), path(&dif_mbt)).await?; let hash_v2 = tar2_mbt.validate(Off, false).await?; allow_duplicates! { @@ -305,17 +333,56 @@ async fn diff_apply( Ok(()) } -// /// A simple tester to run specific values -// #[actix_rt::test] -// async fn test_one() { -// let dif_type = FlatWithHash; -// let src_type = Flat; -// let dst_type = Some(Normalized); -// let db = databases(); -// -// diff_apply(src_type, dif_type, dst_type, &db).await.unwrap(); -// panic!() -// } +#[rstest] +#[trace] +#[actix_rt::test] +async fn patch_on_copy( + #[values(Flat, FlatWithHash, Normalized)] v1_type: MbtTypeCli, + #[values(Flat, FlatWithHash, Normalized)] dif_type: MbtTypeCli, + #[values(None, Some(Flat), Some(FlatWithHash), Some(Normalized))] v2_type: Option, + #[notrace] databases: &Databases, +) -> MbtResult<()> { + let (v1, dif) = (shorten(v1_type), shorten(dif_type)); + let v2 = v2_type.map(shorten).unwrap_or("dflt"); + let prefix = format!("{v1}+{dif}={v2}"); + + let v1_mbt = databases.mbtiles("v1", v1_type); + let dif_mbt = databases.mbtiles("dif", dif_type); + let (v2_mbt, mut v2_cn) = open!(patch_on_copy, "{prefix}__v2"); + + info!("TEST: Compare v1 with v2, and copy anything that's different (i.e. mathematically: v2-v1=diff)"); + let mut opt = copier(v1_mbt, &v2_mbt); + opt.apply_patch = Some(path(dif_mbt)); + if let Some(v2_type) = v2_type { + opt.dst_type_cli = Some(v2_type); + } + opt.run().await?; + pretty_assert_eq!( + &dump(&mut v2_cn).await?, + databases.dump("v2", v2_type.unwrap_or(v1_type)) + ); + + Ok(()) +} + +/// A simple tester to run specific values +#[actix_rt::test] +#[ignore] +async fn test_one() { + let src_type = FlatWithHash; + let dif_type = FlatWithHash; + // let dst_type = Some(FlatWithHash); + let dst_type = None; + let db = databases(); + + diff_and_patch(src_type, dif_type, dst_type, &db) + .await + .unwrap(); + patch_on_copy(src_type, dif_type, dst_type, &db) + .await + .unwrap(); + panic!("ALWAYS FAIL - this test is for debugging only, and should be disabled"); +} #[derive(Debug, sqlx::FromRow, Serialize, PartialEq)] struct SqliteEntry { diff --git a/tests/expected/mbtiles/help.txt b/tests/expected/mbtiles/help.txt index a7f0896af..55eb51149 100644 --- a/tests/expected/mbtiles/help.txt +++ b/tests/expected/mbtiles/help.txt @@ -3,13 +3,13 @@ A utility to work with .mbtiles file content Usage: mbtiles Commands: - meta-all Prints all values in the metadata table in a free-style, unstable YAML format - meta-get Gets a single value from the MBTiles metadata table - meta-set Sets a single value in the MBTiles' file metadata table or deletes it if no value - copy Copy tiles from one mbtiles file to another - apply-diff Apply diff file generated from 'copy' command - validate Validate tile data if hash of tile data exists in file - help Print this message or the help of the given subcommand(s) + meta-all Prints all values in the metadata table in a free-style, unstable YAML format + meta-get Gets a single value from the MBTiles metadata table + meta-set Sets a single value in the MBTiles' file metadata table or deletes it if no value + copy Copy tiles from one mbtiles file to another + apply-patch Apply diff file generated from 'copy' command + validate Validate tile data if hash of tile data exists in file + help Print this message or the help of the given subcommand(s) Options: -h, --help Print help From 108bc0504225e9ff7938fe1431150eb2bea1bd2f Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Mon, 16 Oct 2023 21:22:54 -0400 Subject: [PATCH 057/108] pg func discovery ignore sys schemas (#946) * Blessing results should destroy test db * ignore `pg_catalog` and `information_schema` schemas during functions autodiscovery --- justfile | 2 +- martin/src/pg/scripts/query_available_function.sql | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/justfile b/justfile index 9f7d8d106..2a78b4952 100644 --- a/justfile +++ b/justfile @@ -138,7 +138,7 @@ test-int: clean-test install-sqlx fi # Run integration tests and save its output as the new expected output -bless: start clean-test bless-insta +bless: restart clean-test bless-insta rm -rf tests/temp cargo test -p martin --features bless-tests tests/test.sh diff --git a/martin/src/pg/scripts/query_available_function.sql b/martin/src/pg/scripts/query_available_function.sql index 00d65bf5d..baedff978 100755 --- a/martin/src/pg/scripts/query_available_function.sql +++ b/martin/src/pg/scripts/query_available_function.sql @@ -23,6 +23,7 @@ WITH jsonb_agg(data_type::text ORDER BY ordinal_position) as input_types FROM information_schema.parameters WHERE parameter_mode = 'IN' + AND specific_schema NOT IN ('pg_catalog', 'information_schema') GROUP BY specific_name), -- outputs AS ( @@ -32,6 +33,7 @@ WITH jsonb_agg(parameter_name::text ORDER BY ordinal_position) as out_names FROM information_schema.parameters WHERE parameter_mode = 'OUT' + AND specific_schema NOT IN ('pg_catalog', 'information_schema') GROUP BY specific_name), -- comments AS ( From f488ee5795cfb0046ae06ae4ee1efe95097f9f4c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Oct 2023 02:32:51 +0000 Subject: [PATCH 058/108] chore(deps): Bump regex from 1.10.1 to 1.10.2 (#947) Bumps [regex](https://github.com/rust-lang/regex) from 1.10.1 to 1.10.2.
Changelog

Sourced from regex's changelog.

1.10.2 (2023-10-16)

This is a new patch release that fixes a search regression where incorrect matches could be reported.

Bug fixes:

Commits
  • 5f1f1c8 1.10.2
  • 1a54a82 deps: bump regex-automata to 0.4.3
  • 61242b1 regex-automata-0.4.3
  • 50fe7d1 changelog: 1.10.2
  • eb950f6 automata/meta: revert broadening of reverse suffix optimization
  • e7bd19d regex-lite-0.1.5
  • 0086dec lite: fix stack overflow test
  • 4ae1472 tests: fix compilation of doctests on 32-bit architectures
  • cd79881 regex-lite-0.1.4
  • 466e42c lite: fix stack overflow in NFA compiler
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=regex&package-manager=cargo&previous-version=1.10.1&new-version=1.10.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7ffb7265b..b1758b15f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2381,9 +2381,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.1" +version = "1.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaac441002f822bc9705a681810a4dd2963094b9ca0ddc41cb963a4c189189ea" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" dependencies = [ "aho-corasick", "memchr", @@ -2393,9 +2393,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5011c7e263a695dc8ca064cddb722af1be54e517a280b12a5356f98366899e5d" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" dependencies = [ "aho-corasick", "memchr", From 2196065a3fdbf6fb47602f88120801595a4910e7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Oct 2023 14:16:28 -0400 Subject: [PATCH 059/108] chore(deps): Bump @babel/traverse from 7.22.10 to 7.23.2 in /demo/frontend (#948) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.22.10 to 7.23.2.
Release notes

Sourced from @​babel/traverse's releases.

v7.23.2 (2023-10-11)

NOTE: This release also re-publishes @babel/core, even if it does not appear in the linked release commit.

Thanks @​jimmydief for your first PR!

:bug: Bug Fix

  • babel-traverse
  • babel-preset-typescript
  • babel-helpers
    • #16017 Fix: fallback to typeof when toString is applied to incompatible object (@​JLHwung)
  • babel-helpers, babel-plugin-transform-modules-commonjs, babel-runtime-corejs2, babel-runtime-corejs3, babel-runtime

Committers: 5

v7.23.1 (2023-09-25)

Re-publishing @babel/helpers due to a publishing error in 7.23.0.

v7.23.0 (2023-09-25)

Thanks @​lorenzoferre and @​RajShukla1 for your first PRs!

:rocket: New Feature

  • babel-plugin-proposal-import-wasm-source, babel-plugin-syntax-import-source, babel-plugin-transform-dynamic-import
  • babel-helper-module-transforms, babel-helpers, babel-plugin-proposal-import-defer, babel-plugin-syntax-import-defer, babel-plugin-transform-modules-commonjs, babel-runtime-corejs2, babel-runtime-corejs3, babel-runtime, babel-standalone
  • babel-generator, babel-parser, babel-types
  • babel-generator, babel-helper-module-transforms, babel-parser, babel-plugin-transform-dynamic-import, babel-plugin-transform-modules-amd, babel-plugin-transform-modules-commonjs, babel-plugin-transform-modules-systemjs, babel-traverse, babel-types
  • babel-standalone
  • babel-helper-function-name, babel-helper-member-expression-to-functions, babel-helpers, babel-parser, babel-plugin-proposal-destructuring-private, babel-plugin-proposal-optional-chaining-assign, babel-plugin-syntax-optional-chaining-assign, babel-plugin-transform-destructuring, babel-plugin-transform-optional-chaining, babel-runtime-corejs2, babel-runtime-corejs3, babel-runtime, babel-standalone, babel-types
  • babel-helpers, babel-plugin-proposal-decorators
  • babel-traverse, babel-types
  • babel-preset-typescript

... (truncated)

Changelog

Sourced from @​babel/traverse's changelog.

v7.23.2 (2023-10-11)

:bug: Bug Fix

  • babel-traverse
  • babel-preset-typescript
  • babel-helpers
    • #16017 Fix: fallback to typeof when toString is applied to incompatible object (@​JLHwung)
  • babel-helpers, babel-plugin-transform-modules-commonjs, babel-runtime-corejs2, babel-runtime-corejs3, babel-runtime

v7.23.0 (2023-09-25)

:rocket: New Feature

  • babel-plugin-proposal-import-wasm-source, babel-plugin-syntax-import-source, babel-plugin-transform-dynamic-import
  • babel-helper-module-transforms, babel-helpers, babel-plugin-proposal-import-defer, babel-plugin-syntax-import-defer, babel-plugin-transform-modules-commonjs, babel-runtime-corejs2, babel-runtime-corejs3, babel-runtime, babel-standalone
  • babel-generator, babel-parser, babel-types
  • babel-generator, babel-helper-module-transforms, babel-parser, babel-plugin-transform-dynamic-import, babel-plugin-transform-modules-amd, babel-plugin-transform-modules-commonjs, babel-plugin-transform-modules-systemjs, babel-traverse, babel-types
  • babel-standalone
  • babel-helper-function-name, babel-helper-member-expression-to-functions, babel-helpers, babel-parser, babel-plugin-proposal-destructuring-private, babel-plugin-proposal-optional-chaining-assign, babel-plugin-syntax-optional-chaining-assign, babel-plugin-transform-destructuring, babel-plugin-transform-optional-chaining, babel-runtime-corejs2, babel-runtime-corejs3, babel-runtime, babel-standalone, babel-types
  • babel-helpers, babel-plugin-proposal-decorators
  • babel-traverse, babel-types
  • babel-preset-typescript
  • babel-parser

:bug: Bug Fix

  • babel-plugin-transform-block-scoping

:nail_care: Polish

  • babel-traverse
  • babel-plugin-proposal-explicit-resource-management

:microscope: Output optimization

  • babel-core, babel-helper-module-transforms, babel-plugin-transform-async-to-generator, babel-plugin-transform-classes, babel-plugin-transform-dynamic-import, babel-plugin-transform-function-name, babel-plugin-transform-modules-amd, babel-plugin-transform-modules-commonjs, babel-plugin-transform-modules-umd, babel-plugin-transform-parameters, babel-plugin-transform-react-constant-elements, babel-plugin-transform-react-inline-elements, babel-plugin-transform-runtime, babel-plugin-transform-typescript, babel-preset-env

v7.22.20 (2023-09-16)

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@babel/traverse&package-manager=npm_and_yarn&previous-version=7.22.10&new-version=7.23.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/maplibre/martin/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- demo/frontend/yarn.lock | 88 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 78 insertions(+), 10 deletions(-) diff --git a/demo/frontend/yarn.lock b/demo/frontend/yarn.lock index c3b4ffff1..c79e0e5d1 100644 --- a/demo/frontend/yarn.lock +++ b/demo/frontend/yarn.lock @@ -23,6 +23,14 @@ "@babel/highlight" "^7.22.10" chalk "^2.4.2" +"@babel/code-frame@^7.22.13": + version "7.22.13" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e" + integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w== + dependencies: + "@babel/highlight" "^7.22.13" + chalk "^2.4.2" + "@babel/compat-data@^7.22.5", "@babel/compat-data@^7.22.6", "@babel/compat-data@^7.22.9": version "7.22.9" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.22.9.tgz#71cdb00a1ce3a329ce4cbec3a44f9fef35669730" @@ -68,6 +76,16 @@ "@jridgewell/trace-mapping" "^0.3.17" jsesc "^2.5.1" +"@babel/generator@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.0.tgz#df5c386e2218be505b34837acbcb874d7a983420" + integrity sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g== + dependencies: + "@babel/types" "^7.23.0" + "@jridgewell/gen-mapping" "^0.3.2" + "@jridgewell/trace-mapping" "^0.3.17" + jsesc "^2.5.1" + "@babel/helper-annotate-as-pure@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz#e7f06737b197d580a01edf75d97e2c8be99d3882" @@ -128,6 +146,11 @@ lodash.debounce "^4.0.8" resolve "^1.14.2" +"@babel/helper-environment-visitor@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167" + integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA== + "@babel/helper-environment-visitor@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz#f06dd41b7c1f44e1f8da6c4055b41ab3a09a7e98" @@ -141,6 +164,14 @@ "@babel/template" "^7.22.5" "@babel/types" "^7.22.5" +"@babel/helper-function-name@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759" + integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw== + dependencies: + "@babel/template" "^7.22.15" + "@babel/types" "^7.23.0" + "@babel/helper-hoist-variables@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb" @@ -229,6 +260,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f" integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw== +"@babel/helper-validator-identifier@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" + integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== + "@babel/helper-validator-identifier@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz#9544ef6a33999343c8740fa51350f30eeaaaf193" @@ -266,11 +302,25 @@ chalk "^2.4.2" js-tokens "^4.0.0" +"@babel/highlight@^7.22.13": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.20.tgz#4ca92b71d80554b01427815e06f2df965b9c1f54" + integrity sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg== + dependencies: + "@babel/helper-validator-identifier" "^7.22.20" + chalk "^2.4.2" + js-tokens "^4.0.0" + "@babel/parser@^7.22.10", "@babel/parser@^7.22.5": version "7.22.10" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.10.tgz#e37634f9a12a1716136c44624ef54283cabd3f55" integrity sha512-lNbdGsQb9ekfsnjFGhEiF4hfFqGgfOP3H3d27re3n+CGhNuTSUEQdfWk556sTLNTloczcdM5TYF2LhzmDQKyvQ== +"@babel/parser@^7.22.15", "@babel/parser@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.0.tgz#da950e622420bf96ca0d0f2909cdddac3acd8719" + integrity sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw== + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.5.tgz#87245a21cd69a73b0b81bcda98d443d6df08f05e" @@ -1094,6 +1144,15 @@ dependencies: regenerator-runtime "^0.14.0" +"@babel/template@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" + integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w== + dependencies: + "@babel/code-frame" "^7.22.13" + "@babel/parser" "^7.22.15" + "@babel/types" "^7.22.15" + "@babel/template@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.5.tgz#0c8c4d944509875849bd0344ff0050756eefc6ec" @@ -1104,18 +1163,18 @@ "@babel/types" "^7.22.5" "@babel/traverse@^7.22.10", "@babel/traverse@^7.4.5": - version "7.22.10" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.22.10.tgz#20252acb240e746d27c2e82b4484f199cf8141aa" - integrity sha512-Q/urqV4pRByiNNpb/f5OSv28ZlGJiFiiTh+GAHktbIrkPhPbl90+uW6SmpoLyZqutrg9AEaEf3Q/ZBRHBXgxig== - dependencies: - "@babel/code-frame" "^7.22.10" - "@babel/generator" "^7.22.10" - "@babel/helper-environment-visitor" "^7.22.5" - "@babel/helper-function-name" "^7.22.5" + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8" + integrity sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw== + dependencies: + "@babel/code-frame" "^7.22.13" + "@babel/generator" "^7.23.0" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-function-name" "^7.23.0" "@babel/helper-hoist-variables" "^7.22.5" "@babel/helper-split-export-declaration" "^7.22.6" - "@babel/parser" "^7.22.10" - "@babel/types" "^7.22.10" + "@babel/parser" "^7.23.0" + "@babel/types" "^7.23.0" debug "^4.1.0" globals "^11.1.0" @@ -1128,6 +1187,15 @@ "@babel/helper-validator-identifier" "^7.22.5" to-fast-properties "^2.0.0" +"@babel/types@^7.22.15", "@babel/types@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.0.tgz#8c1f020c9df0e737e4e247c0619f58c68458aaeb" + integrity sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg== + dependencies: + "@babel/helper-string-parser" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.20" + to-fast-properties "^2.0.0" + "@emotion/is-prop-valid@^1.1.0": version "1.2.1" resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz#23116cf1ed18bfeac910ec6436561ecb1a3885cc" From a0a3f494080d8ebd4be18409d5788d21eb9a11c0 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Tue, 17 Oct 2023 23:05:14 -0400 Subject: [PATCH 060/108] remove unused actix crate dep --- Cargo.lock | 37 ------------------------------------- Cargo.toml | 1 - martin/Cargo.toml | 1 - 3 files changed, 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b1758b15f..c37df068a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,31 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "actix" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cba56612922b907719d4a01cf11c8d5b458e7d3dba946d0435f20f58d6795ed2" -dependencies = [ - "actix-macros", - "actix-rt", - "actix_derive", - "bitflags 2.4.1", - "bytes", - "crossbeam-channel", - "futures-core", - "futures-sink", - "futures-task", - "futures-util", - "log", - "once_cell", - "parking_lot", - "pin-project-lite", - "smallvec", - "tokio", - "tokio-util", -] - [[package]] name = "actix-codec" version = "0.5.1" @@ -222,17 +197,6 @@ dependencies = [ "syn 2.0.38", ] -[[package]] -name = "actix_derive" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c7db3d5a9718568e4cf4a537cfd7070e6e6ff7481510d0237fb529ac850f6d3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.38", -] - [[package]] name = "addr2line" version = "0.21.0" @@ -1731,7 +1695,6 @@ checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" name = "martin" version = "0.9.3" dependencies = [ - "actix", "actix-cors", "actix-http", "actix-rt", diff --git a/Cargo.toml b/Cargo.toml index 9531ce2e1..ad632147f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,6 @@ readme = "README.md" homepage = "https://martin.maplibre.org/" [workspace.dependencies] -actix = "0.13" actix-cors = "0.6" actix-http = "3" actix-rt = "2" diff --git a/martin/Cargo.toml b/martin/Cargo.toml index 750dca484..c1ca06366 100644 --- a/martin/Cargo.toml +++ b/martin/Cargo.toml @@ -55,7 +55,6 @@ actix-cors.workspace = true actix-http.workspace = true actix-rt.workspace = true actix-web.workspace = true -actix.workspace = true async-trait.workspace = true brotli.workspace = true clap.workspace = true From addefbd50c075962efaa843ec7b4ae7c78052232 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Wed, 18 Oct 2023 01:00:37 -0400 Subject: [PATCH 061/108] bump cargo.lock --- Cargo.lock | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c37df068a..0994f5c70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1046,7 +1046,7 @@ checksum = "d4029edd3e734da6fe05b6cd7bd2960760a616bd2ddd0d59a0124746d6272af0" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.3.5", "windows-sys 0.48.0", ] @@ -1677,9 +1677,9 @@ checksum = "e34f76eb3611940e0e7d53a9aaa4e6a3151f69541a282fd0dad5571420c53ff1" [[package]] name = "lock_api" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" dependencies = [ "autocfg", "scopeguard", @@ -1991,13 +1991,13 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.4.1", "smallvec", "windows-targets 0.48.5", ] @@ -2342,6 +2342,15 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "regex" version = "1.10.2" @@ -3210,7 +3219,7 @@ checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" dependencies = [ "cfg-if", "fastrand", - "redox_syscall", + "redox_syscall 0.3.5", "rustix", "windows-sys 0.48.0", ] From c2a12a3799f92d1430b95aa0e4b28e9576b48871 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Thu, 19 Oct 2023 02:13:52 -0400 Subject: [PATCH 062/108] Implement sprite catalog reporting (#951) The `/catalog` now shows available sprites, which also paves way for the future font support. Lots of small refactorings to streamline tile source management. Now each tile source can produce its own catalog entry, making the whole thing much simpler. Fixes #949 --- martin/src/config.rs | 37 +++--- martin/src/file_config.rs | 14 +-- martin/src/lib.rs | 5 +- martin/src/mbtiles/mod.rs | 13 +- martin/src/pg/config.rs | 4 +- martin/src/pg/configurator.rs | 28 +++-- martin/src/pg/pg_source.rs | 16 +-- martin/src/pmtiles/mod.rs | 17 ++- martin/src/source.rs | 121 +++++++----------- martin/src/sprites/mod.rs | 136 ++++++++++++--------- martin/src/srv/mod.rs | 2 +- martin/src/srv/server.rs | 68 +++++++---- martin/src/utils/mod.rs | 2 + martin/src/utils/utilities.rs | 6 - martin/src/utils/xyz.rs | 18 +++ tests/expected/auto/catalog_auto.json | 3 +- tests/expected/configured/catalog_cfg.json | 14 +++ 17 files changed, 271 insertions(+), 233 deletions(-) create mode 100644 martin/src/utils/xyz.rs diff --git a/martin/src/config.rs b/martin/src/config.rs index b477fd6f4..5856bed23 100644 --- a/martin/src/config.rs +++ b/martin/src/config.rs @@ -13,16 +13,16 @@ use crate::file_config::{resolve_files, FileConfigEnum}; use crate::mbtiles::MbtSource; use crate::pg::PgConfig; use crate::pmtiles::PmtSource; -use crate::source::Sources; -use crate::sprites::{resolve_sprites, SpriteSources}; +use crate::source::{TileInfoSources, TileSources}; +use crate::sprites::SpriteSources; use crate::srv::SrvConfig; -use crate::utils::{IdResolver, OneOrMany, Result}; use crate::Error::{ConfigLoadError, ConfigParseError, NoSources}; +use crate::{IdResolver, OneOrMany, Result}; pub type UnrecognizedValues = HashMap; -pub struct AllSources { - pub sources: Sources, +pub struct ServerState { + pub tiles: TileSources, pub sprites: SpriteSources, } @@ -90,16 +90,24 @@ impl Config { } } - pub async fn resolve(&mut self, idr: IdResolver) -> Result { + pub async fn resolve(&mut self, idr: IdResolver) -> Result { + Ok(ServerState { + tiles: self.resolve_tile_sources(idr).await?, + sprites: SpriteSources::resolve(&mut self.sprites)?, + }) + } + + async fn resolve_tile_sources(&mut self, idr: IdResolver) -> Result { let create_pmt_src = &mut PmtSource::new_box; let create_mbt_src = &mut MbtSource::new_box; + let mut sources: Vec>>>> = Vec::new(); - let mut sources: Vec>>>> = Vec::new(); if let Some(v) = self.postgres.as_mut() { for s in v.iter_mut() { sources.push(Box::pin(s.resolve(idr.clone()))); } } + if self.pmtiles.is_some() { let val = resolve_files(&mut self.pmtiles, idr.clone(), "pmtiles", create_pmt_src); sources.push(Box::pin(val)); @@ -110,20 +118,7 @@ impl Config { sources.push(Box::pin(val)); } - // Minor in-efficiency: - // Sources are added to a BTreeMap, then iterated over into a sort structure and convert back to a BTreeMap. - // Ideally there should be a vector of values, which is then sorted (in-place?) and converted to a BTreeMap. - Ok(AllSources { - sources: try_join_all(sources) - .await? - .into_iter() - .fold(Sources::default(), |mut acc, hashmap| { - acc.extend(hashmap); - acc - }) - .sort(), - sprites: resolve_sprites(&mut self.sprites)?, - }) + Ok(TileSources::new(try_join_all(sources).await?)) } } diff --git a/martin/src/file_config.rs b/martin/src/file_config.rs index b290356cf..9cb55a359 100644 --- a/martin/src/file_config.rs +++ b/martin/src/file_config.rs @@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize}; use crate::config::{copy_unrecognized_config, UnrecognizedValues}; use crate::file_config::FileError::{InvalidFilePath, InvalidSourceFilePath, IoError}; -use crate::source::{Source, Sources}; +use crate::source::{Source, TileInfoSources}; use crate::utils::{sorted_opt_map, Error, IdResolver, OneOrMany}; use crate::OneOrMany::{Many, One}; @@ -152,7 +152,7 @@ pub async fn resolve_files( idr: IdResolver, extension: &str, create_source: &mut impl FnMut(String, PathBuf) -> Fut, -) -> Result +) -> Result where Fut: Future, FileError>>, { @@ -166,16 +166,16 @@ async fn resolve_int( idr: IdResolver, extension: &str, create_source: &mut impl FnMut(String, PathBuf) -> Fut, -) -> Result +) -> Result where Fut: Future, FileError>>, { let Some(cfg) = config else { - return Ok(Sources::default()); + return Ok(TileInfoSources::default()); }; let cfg = cfg.extract_file_config(); - let mut results = Sources::default(); + let mut results = TileInfoSources::default(); let mut configs = HashMap::new(); let mut files = HashSet::new(); let mut directories = Vec::new(); @@ -198,7 +198,7 @@ where FileConfigSrc::Obj(pmt) => pmt.path, FileConfigSrc::Path(path) => path, }; - results.insert(id.clone(), create_source(id, path).await?); + results.push(create_source(id, path).await?); } } @@ -244,7 +244,7 @@ where FileConfigSrc::Obj(pmt) => pmt.path, FileConfigSrc::Path(path) => path, }; - results.insert(id.clone(), create_source(id, path).await?); + results.push(create_source(id, path).await?); } } } diff --git a/martin/src/lib.rs b/martin/src/lib.rs index bdc115c76..6915d0517 100644 --- a/martin/src/lib.rs +++ b/martin/src/lib.rs @@ -18,6 +18,7 @@ mod source; pub mod sprites; pub mod srv; mod utils; +pub use utils::Xyz; #[cfg(test)] #[path = "utils/test_utils.rs"] @@ -27,8 +28,8 @@ mod test_utils; // Must make it accessible as carte::Env from both places when testing. #[cfg(test)] pub use crate::args::Env; -pub use crate::config::{read_config, Config}; -pub use crate::source::{Source, Sources, Xyz}; +pub use crate::config::{read_config, Config, ServerState}; +pub use crate::source::Source; pub use crate::utils::{ decode_brotli, decode_gzip, BoolOrObject, Error, IdResolver, OneOrMany, Result, }; diff --git a/martin/src/mbtiles/mod.rs b/martin/src/mbtiles/mod.rs index 79dbb50e3..efbd74c94 100644 --- a/martin/src/mbtiles/mod.rs +++ b/martin/src/mbtiles/mod.rs @@ -12,7 +12,6 @@ use tilejson::TileJSON; use crate::file_config::FileError; use crate::file_config::FileError::{AquireConnError, InvalidMetadata, IoError}; use crate::source::{Tile, UrlQuery}; -use crate::utils::is_valid_zoom; use crate::{Error, Source, Xyz}; #[derive(Clone)] @@ -66,8 +65,12 @@ impl MbtSource { #[async_trait] impl Source for MbtSource { - fn get_tilejson(&self) -> TileJSON { - self.tilejson.clone() + fn get_id(&self) -> &str { + &self.id + } + + fn get_tilejson(&self) -> &TileJSON { + &self.tilejson } fn get_tile_info(&self) -> TileInfo { @@ -78,10 +81,6 @@ impl Source for MbtSource { Box::new(self.clone()) } - fn is_valid_zoom(&self, zoom: u8) -> bool { - is_valid_zoom(zoom, self.tilejson.minzoom, self.tilejson.maxzoom) - } - fn support_url_query(&self) -> bool { false } diff --git a/martin/src/pg/config.rs b/martin/src/pg/config.rs index 1a46bb90c..1934756cb 100644 --- a/martin/src/pg/config.rs +++ b/martin/src/pg/config.rs @@ -10,7 +10,7 @@ use crate::pg::config_function::FuncInfoSources; use crate::pg::config_table::TableInfoSources; use crate::pg::configurator::PgBuilder; use crate::pg::Result; -use crate::source::Sources; +use crate::source::TileInfoSources; use crate::utils::{on_slow, sorted_opt_map, BoolOrObject, IdResolver, OneOrMany}; pub trait PgInfo { @@ -111,7 +111,7 @@ impl PgConfig { Ok(res) } - pub async fn resolve(&mut self, id_resolver: IdResolver) -> crate::Result { + pub async fn resolve(&mut self, id_resolver: IdResolver) -> crate::Result { let pg = PgBuilder::new(self, id_resolver).await?; let inst_tables = on_slow(pg.instantiate_tables(), Duration::from_secs(5), || { if pg.disable_bounds() { diff --git a/martin/src/pg/configurator.rs b/martin/src/pg/configurator.rs index 5acdeb65b..0cd51d615 100755 --- a/martin/src/pg/configurator.rs +++ b/martin/src/pg/configurator.rs @@ -17,7 +17,7 @@ use crate::pg::table_source::{ use crate::pg::utils::{find_info, find_kv_ignore_case, normalize_key, InfoMap}; use crate::pg::PgError::InvalidTableExtent; use crate::pg::Result; -use crate::source::Sources; +use crate::source::TileInfoSources; use crate::utils::{BoolOrObject, IdResolver, OneOrMany}; pub type SqlFuncInfoMapMap = InfoMap>; @@ -73,7 +73,7 @@ impl PgBuilder { // FIXME: this function has gotten too long due to the new formatting rules, need to be refactored #[allow(clippy::too_many_lines)] - pub async fn instantiate_tables(&self) -> Result<(Sources, TableInfoSources)> { + pub async fn instantiate_tables(&self) -> Result<(TileInfoSources, TableInfoSources)> { let mut db_tables_info = query_available_tables(&self.pool).await?; // Match configured sources with the discovered ones and add them to the pending list. @@ -170,7 +170,7 @@ impl PgBuilder { } } - let mut res = Sources::default(); + let mut res = TileInfoSources::default(); let mut info_map = TableInfoSources::new(); let pending = join_all(pending).await; for src in pending { @@ -190,9 +190,9 @@ impl PgBuilder { Ok((res, info_map)) } - pub async fn instantiate_functions(&self) -> Result<(Sources, FuncInfoSources)> { + pub async fn instantiate_functions(&self) -> Result<(TileInfoSources, FuncInfoSources)> { let mut db_funcs_info = query_available_function(&self.pool).await?; - let mut res = Sources::default(); + let mut res = TileInfoSources::default(); let mut info_map = FuncInfoSources::new(); let mut used = HashSet::<(&str, &str)>::new(); @@ -262,14 +262,16 @@ impl PgBuilder { self.id_resolver.resolve(id, signature) } - fn add_func_src(&self, sources: &mut Sources, id: String, info: &impl PgInfo, sql: PgSqlInfo) { - let source = PgSource::new( - id.clone(), - sql, - info.to_tilejson(id.clone()), - self.pool.clone(), - ); - sources.insert(id, Box::new(source)); + fn add_func_src( + &self, + sources: &mut TileInfoSources, + id: String, + info: &impl PgInfo, + sql: PgSqlInfo, + ) { + let tilejson = info.to_tilejson(id.clone()); + let source = PgSource::new(id, sql, tilejson, self.pool.clone()); + sources.push(Box::new(source)); } } diff --git a/martin/src/pg/pg_source.rs b/martin/src/pg/pg_source.rs index 05d7090ea..735d747af 100644 --- a/martin/src/pg/pg_source.rs +++ b/martin/src/pg/pg_source.rs @@ -11,8 +11,8 @@ use tilejson::TileJSON; use crate::pg::pool::PgPool; use crate::pg::utils::query_to_json; use crate::pg::PgError::{GetTileError, GetTileWithQueryError, PrepareQueryError}; -use crate::source::{Source, Tile, UrlQuery, Xyz}; -use crate::utils::{is_valid_zoom, Result}; +use crate::source::{Source, Tile, UrlQuery}; +use crate::{Result, Xyz}; #[derive(Clone, Debug)] pub struct PgSource { @@ -36,8 +36,12 @@ impl PgSource { #[async_trait] impl Source for PgSource { - fn get_tilejson(&self) -> TileJSON { - self.tilejson.clone() + fn get_id(&self) -> &str { + &self.id + } + + fn get_tilejson(&self) -> &TileJSON { + &self.tilejson } fn get_tile_info(&self) -> TileInfo { @@ -48,10 +52,6 @@ impl Source for PgSource { Box::new(self.clone()) } - fn is_valid_zoom(&self, zoom: u8) -> bool { - is_valid_zoom(zoom, self.tilejson.minzoom, self.tilejson.maxzoom) - } - fn support_url_query(&self) -> bool { self.info.use_url_query } diff --git a/martin/src/pmtiles/mod.rs b/martin/src/pmtiles/mod.rs index 98e3a86b6..2af8cf9e5 100644 --- a/martin/src/pmtiles/mod.rs +++ b/martin/src/pmtiles/mod.rs @@ -13,9 +13,8 @@ use tilejson::TileJSON; use crate::file_config::FileError; use crate::file_config::FileError::{InvalidMetadata, IoError}; -use crate::source::{Source, Tile, UrlQuery, Xyz}; -use crate::utils::is_valid_zoom; -use crate::Error; +use crate::source::{Source, Tile, UrlQuery}; +use crate::{Error, Xyz}; #[derive(Clone)] pub struct PmtSource { @@ -114,8 +113,12 @@ impl PmtSource { #[async_trait] impl Source for PmtSource { - fn get_tilejson(&self) -> TileJSON { - self.tilejson.clone() + fn get_id(&self) -> &str { + &self.id + } + + fn get_tilejson(&self) -> &TileJSON { + &self.tilejson } fn get_tile_info(&self) -> TileInfo { @@ -126,10 +129,6 @@ impl Source for PmtSource { Box::new(self.clone()) } - fn is_valid_zoom(&self, zoom: u8) -> bool { - is_valid_zoom(zoom, self.tilejson.minzoom, self.tilejson.maxzoom) - } - fn support_url_query(&self) -> bool { false } diff --git a/martin/src/source.rs b/martin/src/source.rs index 0292166fb..bc6767120 100644 --- a/martin/src/source.rs +++ b/martin/src/source.rs @@ -1,5 +1,5 @@ use std::collections::{BTreeMap, HashMap}; -use std::fmt::{Debug, Display, Formatter}; +use std::fmt::Debug; use actix_web::error::ErrorNotFound; use async_trait::async_trait; @@ -9,83 +9,42 @@ use martin_tile_utils::TileInfo; use serde::{Deserialize, Serialize}; use tilejson::TileJSON; -use crate::utils::Result; - -#[derive(Debug, Copy, Clone)] -pub struct Xyz { - pub z: u8, - pub x: u32, - pub y: u32, -} - -impl Display for Xyz { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - if f.alternate() { - write!(f, "{}/{}/{}", self.z, self.x, self.y) - } else { - write!(f, "{},{},{}", self.z, self.x, self.y) - } - } -} +use crate::{Result, Xyz}; pub type Tile = Vec; pub type UrlQuery = HashMap; -#[derive(Default, Clone)] -pub struct Sources { - tiles: HashMap>, - catalog: SourceCatalog, -} +pub type TileInfoSource = Box; -impl Sources { - #[must_use] - pub fn sort(self) -> Self { - Self { - tiles: self.tiles, - catalog: SourceCatalog { - tiles: self - .catalog - .tiles - .into_iter() - .sorted_by(|a, b| a.0.cmp(&b.0)) - .collect(), - }, - } - } -} +pub type TileInfoSources = Vec; -impl Sources { - pub fn insert(&mut self, id: String, source: Box) { - let tilejson = source.get_tilejson(); - let info = source.get_tile_info(); - self.catalog.tiles.insert( - id.clone(), - SourceEntry { - content_type: info.format.content_type().to_string(), - content_encoding: info.encoding.content_encoding().map(ToString::to_string), - name: tilejson.name.filter(|v| v != &id), - description: tilejson.description, - attribution: tilejson.attribution, - }, - ); - self.tiles.insert(id, source); - } +#[derive(Default, Clone)] +pub struct TileSources(HashMap>); +pub type TileCatalog = BTreeMap; - pub fn extend(&mut self, other: Sources) { - for (k, v) in other.catalog.tiles { - self.catalog.tiles.insert(k, v); - } - self.tiles.extend(other.tiles); +impl TileSources { + #[must_use] + pub fn new(sources: Vec) -> Self { + Self( + sources + .into_iter() + .flatten() + .map(|src| (src.get_id().to_string(), src)) + .collect(), + ) } - #[must_use] - pub fn get_catalog(&self) -> &SourceCatalog { - &self.catalog + pub fn get_catalog(&self) -> TileCatalog { + self.0 + .iter() + .map(|(id, src)| (id.to_string(), src.get_catalog_entry())) + .sorted_by(|(id1, _), (id2, _)| id1.cmp(id2)) + .collect() } pub fn get_source(&self, id: &str) -> actix_web::Result<&dyn Source> { Ok(self - .tiles + .0 .get(id) .ok_or_else(|| ErrorNotFound(format!("Source {id} does not exist")))? .as_ref()) @@ -138,17 +97,36 @@ impl Sources { #[async_trait] pub trait Source: Send + Debug { - fn get_tilejson(&self) -> TileJSON; + fn get_id(&self) -> &str; + + fn get_tilejson(&self) -> &TileJSON; fn get_tile_info(&self) -> TileInfo; fn clone_source(&self) -> Box; - fn is_valid_zoom(&self, zoom: u8) -> bool; - fn support_url_query(&self) -> bool; async fn get_tile(&self, xyz: &Xyz, query: &Option) -> Result; + + fn is_valid_zoom(&self, zoom: u8) -> bool { + let tj = self.get_tilejson(); + tj.minzoom.map_or(true, |minzoom| zoom >= minzoom) + && tj.maxzoom.map_or(true, |maxzoom| zoom <= maxzoom) + } + + fn get_catalog_entry(&self) -> CatalogSourceEntry { + let id = self.get_id(); + let tilejson = self.get_tilejson(); + let info = self.get_tile_info(); + CatalogSourceEntry { + content_type: info.format.content_type().to_string(), + content_encoding: info.encoding.content_encoding().map(ToString::to_string), + name: tilejson.name.as_ref().filter(|v| *v != id).cloned(), + description: tilejson.description.clone(), + attribution: tilejson.attribution.clone(), + } + } } impl Clone for Box { @@ -158,12 +136,7 @@ impl Clone for Box { } #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] -pub struct SourceCatalog { - tiles: BTreeMap, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] -pub struct SourceEntry { +pub struct CatalogSourceEntry { pub content_type: String, #[serde(skip_serializing_if = "Option::is_none")] pub content_encoding: Option, diff --git a/martin/src/sprites/mod.rs b/martin/src/sprites/mod.rs index c1b5943ed..cb120d4b9 100644 --- a/martin/src/sprites/mod.rs +++ b/martin/src/sprites/mod.rs @@ -1,10 +1,12 @@ use std::collections::hash_map::Entry; -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; use std::fmt::Debug; use std::path::PathBuf; use futures::future::try_join_all; +use itertools::Itertools; use log::{info, warn}; +use serde::{Deserialize, Serialize}; use spreet::fs::get_svg_input_paths; use spreet::resvg::usvg::{Error as ResvgError, Options, Tree, TreeParsing}; use spreet::sprite::{sprite_name, Sprite, Spritesheet, SpritesheetBuilder}; @@ -42,68 +44,86 @@ pub enum SpriteError { UnableToGenerateSpritesheet, } -pub fn resolve_sprites(config: &mut Option) -> Result { - let Some(cfg) = config else { - return Ok(SpriteSources::default()); - }; +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] +pub struct CatalogSpriteEntry { + pub images: Vec, +} - let cfg = cfg.extract_file_config(); - let mut results = SpriteSources::default(); - let mut directories = Vec::new(); - let mut configs = HashMap::new(); +pub type SpriteCatalog = BTreeMap; - if let Some(sources) = cfg.sources { - for (id, source) in sources { - configs.insert(id.clone(), source.clone()); - add_source(id, source.abs_path()?, &mut results); - } - }; - - if let Some(paths) = cfg.paths { - for path in paths { - let Some(name) = path.file_name() else { - warn!( - "Ignoring sprite source with no name from {}", - path.display() - ); - continue; - }; - directories.push(path.clone()); - add_source(name.to_string_lossy().to_string(), path, &mut results); - } - } +#[derive(Debug, Clone, Default)] +pub struct SpriteSources(HashMap); - *config = FileConfigEnum::new_extended(directories, configs, cfg.unrecognized); +impl SpriteSources { + pub fn resolve(config: &mut Option) -> Result { + let Some(cfg) = config else { + return Ok(Self::default()); + }; - Ok(results) -} + let cfg = cfg.extract_file_config(); + let mut results = Self::default(); + let mut directories = Vec::new(); + let mut configs = HashMap::new(); -fn add_source(id: String, path: PathBuf, results: &mut SpriteSources) { - let disp_path = path.display(); - if path.is_file() { - warn!("Ignoring non-directory sprite source {id} from {disp_path}"); - } else { - match results.0.entry(id) { - Entry::Occupied(v) => { - warn!("Ignoring duplicate sprite source {} from {disp_path} because it was already configured for {}", - v.key(), v.get().path.display()); + if let Some(sources) = cfg.sources { + for (id, source) in sources { + configs.insert(id.clone(), source.clone()); + results.add_source(id, source.abs_path()?); } - Entry::Vacant(v) => { - info!("Configured sprite source {} from {disp_path}", v.key()); - v.insert(SpriteSource { path }); + }; + + if let Some(paths) = cfg.paths { + for path in paths { + let Some(name) = path.file_name() else { + warn!( + "Ignoring sprite source with no name from {}", + path.display() + ); + continue; + }; + directories.push(path.clone()); + results.add_source(name.to_string_lossy().to_string(), path); } } - }; -} -#[derive(Debug, Clone, Default)] -pub struct SpriteSources(HashMap); + *config = FileConfigEnum::new_extended(directories, configs, cfg.unrecognized); -impl SpriteSources { - pub fn get_sprite_source(&self, id: &str) -> Result<&SpriteSource, SpriteError> { - self.0 - .get(id) - .ok_or_else(|| SpriteError::SpriteNotFound(id.to_string())) + Ok(results) + } + + pub fn get_catalog(&self) -> Result { + // TODO: all sprite generation should be pre-cached + Ok(self + .0 + .iter() + .sorted_by(|(id1, _), (id2, _)| id1.cmp(id2)) + .map(|(id, source)| { + let mut images = get_svg_input_paths(&source.path, true) + .into_iter() + .map(|svg_path| sprite_name(svg_path, &source.path)) + .collect::>(); + images.sort(); + (id.clone(), CatalogSpriteEntry { images }) + }) + .collect()) + } + + fn add_source(&mut self, id: String, path: PathBuf) { + let disp_path = path.display(); + if path.is_file() { + warn!("Ignoring non-directory sprite source {id} from {disp_path}"); + } else { + match self.0.entry(id) { + Entry::Occupied(v) => { + warn!("Ignoring duplicate sprite source {} from {disp_path} because it was already configured for {}", + v.key(), v.get().path.display()); + } + Entry::Vacant(v) => { + info!("Configured sprite source {} from {disp_path}", v.key()); + v.insert(SpriteSource { path }); + } + } + }; } /// Given a list of IDs in a format "id1,id2,id3", return a spritesheet with them all. @@ -114,10 +134,16 @@ impl SpriteSources { } else { (ids, 1) }; + let sprite_ids = ids .split(',') - .map(|id| self.get_sprite_source(id)) + .map(|id| { + self.0 + .get(id) + .ok_or_else(|| SpriteError::SpriteNotFound(id.to_string())) + }) .collect::, SpriteError>>()?; + get_spritesheet(sprite_ids.into_iter(), dpi).await } } @@ -187,7 +213,7 @@ mod tests { PathBuf::from("../tests/fixtures/sprites/src2"), ]); - let sprites = resolve_sprites(&mut cfg).unwrap().0; + let sprites = SpriteSources::resolve(&mut cfg).unwrap().0; assert_eq!(sprites.len(), 2); test_src(sprites.values(), 1, "all_1").await; diff --git a/martin/src/srv/mod.rs b/martin/src/srv/mod.rs index f88dbe558..c637ca755 100644 --- a/martin/src/srv/mod.rs +++ b/martin/src/srv/mod.rs @@ -4,4 +4,4 @@ mod server; pub use config::{SrvConfig, KEEP_ALIVE_DEFAULT, LISTEN_ADDRESSES_DEFAULT}; pub use server::{new_server, router, RESERVED_KEYWORDS}; -pub use crate::source::SourceEntry; +pub use crate::source::CatalogSourceEntry; diff --git a/martin/src/srv/server.rs b/martin/src/srv/server.rs index 345b992ca..f058a72c0 100755 --- a/martin/src/srv/server.rs +++ b/martin/src/srv/server.rs @@ -17,17 +17,19 @@ use actix_web::{ Result, }; use futures::future::try_join_all; +use itertools::Itertools as _; use log::error; use martin_tile_utils::{Encoding, Format, TileInfo}; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use tilejson::{tilejson, TileJSON}; -use crate::config::AllSources; -use crate::source::{Source, Sources, UrlQuery, Xyz}; -use crate::sprites::{SpriteError, SpriteSources}; +use crate::config::ServerState; +use crate::source::{Source, TileCatalog, TileSources, UrlQuery}; +use crate::sprites::{SpriteCatalog, SpriteError, SpriteSources}; use crate::srv::config::{SrvConfig, KEEP_ALIVE_DEFAULT, LISTEN_ADDRESSES_DEFAULT}; use crate::utils::{decode_brotli, decode_gzip, encode_brotli, encode_gzip}; use crate::Error::BindingError; +use crate::Xyz; /// List of keywords that cannot be used as source IDs. Some of these are reserved for future use. /// Reserved keywords must never end in a "dot number" (e.g. ".1"). @@ -43,6 +45,12 @@ static SUPPORTED_ENCODINGS: &[HeaderEnc] = &[ HeaderEnc::identity(), ]; +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] +pub struct Catalog { + pub tiles: TileCatalog, + pub sprites: SpriteCatalog, +} + #[derive(Deserialize)] struct TileJsonRequest { source_ids: String, @@ -95,8 +103,8 @@ async fn get_health() -> impl Responder { wrap = "middleware::Compress::default()" )] #[allow(clippy::unused_async)] -async fn get_catalog(sources: Data) -> impl Responder { - HttpResponse::Ok().json(sources.get_catalog()) +async fn get_catalog(catalog: Data) -> impl Responder { + HttpResponse::Ok().json(catalog) } #[route("/sprite/{source_ids}.png", method = "GET", method = "HEAD")] @@ -140,7 +148,7 @@ async fn get_sprite_json( async fn git_source_info( req: HttpRequest, path: Path, - sources: Data, + sources: Data, ) -> Result { let sources = sources.get_sources(&path.source_ids, None)?.0; @@ -174,7 +182,7 @@ fn get_tiles_url(scheme: &str, host: &str, query_string: &str, tiles_path: &str) fn merge_tilejson(sources: Vec<&dyn Source>, tiles_url: String) -> TileJSON { if sources.len() == 1 { - let mut tj = sources[0].get_tilejson(); + let mut tj = sources[0].get_tilejson().clone(); tj.tiles = vec![tiles_url]; return tj; } @@ -189,15 +197,15 @@ fn merge_tilejson(sources: Vec<&dyn Source>, tiles_url: String) -> TileJSON { for src in sources { let tj = src.get_tilejson(); - if let Some(vector_layers) = tj.vector_layers { + if let Some(vector_layers) = &tj.vector_layers { if let Some(ref mut a) = result.vector_layers { - a.extend(vector_layers); + a.extend(vector_layers.iter().cloned()); } else { - result.vector_layers = Some(vector_layers); + result.vector_layers = Some(vector_layers.clone()); } } - if let Some(v) = tj.attribution { + if let Some(v) = &tj.attribution { if !attributions.contains(&v) { attributions.push(v); } @@ -216,7 +224,7 @@ fn merge_tilejson(sources: Vec<&dyn Source>, tiles_url: String) -> TileJSON { result.center = tj.center; } - if let Some(v) = tj.description { + if let Some(v) = &tj.description { if !descriptions.contains(&v) { descriptions.push(v); } @@ -242,7 +250,7 @@ fn merge_tilejson(sources: Vec<&dyn Source>, tiles_url: String) -> TileJSON { } } - if let Some(name) = tj.name { + if let Some(name) = &tj.name { if !names.contains(&name) { names.push(name); } @@ -250,15 +258,15 @@ fn merge_tilejson(sources: Vec<&dyn Source>, tiles_url: String) -> TileJSON { } if !attributions.is_empty() { - result.attribution = Some(attributions.join("\n")); + result.attribution = Some(attributions.into_iter().join("\n")); } if !descriptions.is_empty() { - result.description = Some(descriptions.join("\n")); + result.description = Some(descriptions.into_iter().join("\n")); } if !names.is_empty() { - result.name = Some(names.join(",")); + result.name = Some(names.into_iter().join(",")); } result @@ -268,7 +276,7 @@ fn merge_tilejson(sources: Vec<&dyn Source>, tiles_url: String) -> TileJSON { async fn get_tile( req: HttpRequest, path: Path, - sources: Data, + sources: Data, ) -> Result { let xyz = Xyz { z: path.z, @@ -306,7 +314,7 @@ async fn get_tile( let id = &path.source_ids; let zoom = xyz.z; let src = sources.get_source(id)?; - if !Sources::check_zoom(src, id, zoom) { + if !TileSources::check_zoom(src, id, zoom) { return Err(ErrorNotFound(format!( "Zoom {zoom} is not valid for source {id}", ))); @@ -413,21 +421,27 @@ pub fn router(cfg: &mut web::ServiceConfig) { } /// Create a new initialized Actix `App` instance together with the listening address. -pub fn new_server(config: SrvConfig, all_sources: AllSources) -> crate::Result<(Server, String)> { +pub fn new_server(config: SrvConfig, all_sources: ServerState) -> crate::Result<(Server, String)> { let keep_alive = Duration::from_secs(config.keep_alive.unwrap_or(KEEP_ALIVE_DEFAULT)); let worker_processes = config.worker_processes.unwrap_or_else(num_cpus::get); let listen_addresses = config .listen_addresses .unwrap_or_else(|| LISTEN_ADDRESSES_DEFAULT.to_owned()); + let catalog = Catalog { + tiles: all_sources.tiles.get_catalog(), + sprites: all_sources.sprites.get_catalog()?, + }; + let server = HttpServer::new(move || { let cors_middleware = Cors::default() .allow_any_origin() .allowed_methods(vec!["GET"]); App::new() - .app_data(Data::new(all_sources.sources.clone())) + .app_data(Data::new(all_sources.tiles.clone())) .app_data(Data::new(all_sources.sprites.clone())) + .app_data(Data::new(catalog.clone())) .wrap(cors_middleware) .wrap(middleware::NormalizePath::new(TrailingSlash::MergeOnly)) .wrap(middleware::Logger::default()) @@ -469,19 +483,19 @@ mod tests { #[async_trait] impl Source for TestSource { - fn get_tilejson(&self) -> TileJSON { - self.tj.clone() + fn get_id(&self) -> &str { + "id" } - fn get_tile_info(&self) -> TileInfo { - unimplemented!() + fn get_tilejson(&self) -> &TileJSON { + &self.tj } - fn clone_source(&self) -> Box { + fn get_tile_info(&self) -> TileInfo { unimplemented!() } - fn is_valid_zoom(&self, _zoom: u8) -> bool { + fn clone_source(&self) -> Box { unimplemented!() } diff --git a/martin/src/utils/mod.rs b/martin/src/utils/mod.rs index 43aff165d..85534e500 100644 --- a/martin/src/utils/mod.rs +++ b/martin/src/utils/mod.rs @@ -2,8 +2,10 @@ mod error; mod id_resolver; mod one_or_many; mod utilities; +mod xyz; pub use error::*; pub use id_resolver::IdResolver; pub use one_or_many::OneOrMany; pub use utilities::*; +pub use xyz::Xyz; diff --git a/martin/src/utils/utilities.rs b/martin/src/utils/utilities.rs index 6635990bb..e05e79276 100644 --- a/martin/src/utils/utilities.rs +++ b/martin/src/utils/utilities.rs @@ -9,12 +9,6 @@ use futures::pin_mut; use serde::{Deserialize, Serialize, Serializer}; use tokio::time::timeout; -#[must_use] -pub fn is_valid_zoom(zoom: u8, minzoom: Option, maxzoom: Option) -> bool { - minzoom.map_or(true, |minzoom| zoom >= minzoom) - && maxzoom.map_or(true, |maxzoom| zoom <= maxzoom) -} - /// A serde helper to store a boolean as an object. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(untagged)] diff --git a/martin/src/utils/xyz.rs b/martin/src/utils/xyz.rs new file mode 100644 index 000000000..599ebd5e6 --- /dev/null +++ b/martin/src/utils/xyz.rs @@ -0,0 +1,18 @@ +use std::fmt::{Display, Formatter}; + +#[derive(Debug, Copy, Clone)] +pub struct Xyz { + pub z: u8, + pub x: u32, + pub y: u32, +} + +impl Display for Xyz { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + if f.alternate() { + write!(f, "{}/{}/{}", self.z, self.x, self.y) + } else { + write!(f, "{},{},{}", self.z, self.x, self.y) + } + } +} diff --git a/tests/expected/auto/catalog_auto.json b/tests/expected/auto/catalog_auto.json index 1de39eb3b..3ce4162f5 100644 --- a/tests/expected/auto/catalog_auto.json +++ b/tests/expected/auto/catalog_auto.json @@ -162,5 +162,6 @@ "name": "Major cities from Natural Earth data", "description": "Major cities from Natural Earth data" } - } + }, + "sprites": {} } diff --git a/tests/expected/configured/catalog_cfg.json b/tests/expected/configured/catalog_cfg.json index 03372aca3..2fb48ab5d 100644 --- a/tests/expected/configured/catalog_cfg.json +++ b/tests/expected/configured/catalog_cfg.json @@ -39,5 +39,19 @@ "content_type": "application/x-protobuf", "description": "public.table_source.geom" } + }, + "sprites": { + "mysrc": { + "images": [ + "bicycle" + ] + }, + "src1": { + "images": [ + "another_bicycle", + "bear", + "sub/circle" + ] + } } } From aadd20cabbd0771abed55cb89015ba48f244b560 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Thu, 19 Oct 2023 11:08:21 -0400 Subject: [PATCH 063/108] bump cargo.lock --- Cargo.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0994f5c70..e757151ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2530,9 +2530,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.19" +version = "0.38.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "745ecfa778e66b2b63c88a61cb36e0eea109e803b0b86bf9879fbc77c70e86ed" +checksum = "67ce50cb2e16c2903e30d1cbccfd8387a74b9d4c938b6a4c5ec6cc7556f7a8a0" dependencies = [ "bitflags 2.4.1", "errno", @@ -3460,9 +3460,9 @@ dependencies = [ [[package]] name = "tracing" -version = "0.1.39" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee2ef2af84856a50c1d430afce2fdded0a4ec7eda868db86409b4543df0797f9" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ "log", "pin-project-lite", @@ -3684,9 +3684,9 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "uuid" -version = "1.4.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" +checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" [[package]] name = "varint-rs" From 467a265e1e0646596a1014001c223c3b45d3ee25 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Oct 2023 03:01:46 +0000 Subject: [PATCH 064/108] chore(deps): Bump thiserror from 1.0.49 to 1.0.50 (#953) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.49 to 1.0.50.
Release notes

Sourced from thiserror's releases.

1.0.50

  • Improve diagnostic when a #[source], #[from], or #[transparant] attribute refers to a type that has no std::error::Error impl (#258, thanks @​de-vri-es)
Commits
  • a7d220d Release 1.0.50
  • 4088d16 Ignore module_name_repetitions pedantic clippy lint
  • ebebf77 Format ui tests with rustfmt
  • ff0a0a5 Source and From attributes only have single-ident path
  • 7cec716 Remove reliance on Spanned for Member
  • c9fe739 Touch up PR 258
  • 4850c6f Merge pull request #258 from de-vri-es/as-dyn-error-span
  • a49f7c6 Change span of as_dyn_error() to point compile error at attribute.
  • f4eac7e Ignore needless_raw_string_hashes clippy lint
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=thiserror&package-manager=cargo&previous-version=1.0.49&new-version=1.0.50)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e757151ae..2e334e29a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3235,18 +3235,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.49" +version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" +checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.49" +version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" +checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" dependencies = [ "proc-macro2", "quote", From 3ef969bc1a629ea4a204c7a13d631542035b61c7 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Thu, 19 Oct 2023 23:10:21 -0400 Subject: [PATCH 065/108] update lock --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2e334e29a..ae55c4c0b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1338,9 +1338,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.14.1" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" +checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" dependencies = [ "ahash", "allocator-api2", @@ -1352,7 +1352,7 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" dependencies = [ - "hashbrown 0.14.1", + "hashbrown 0.14.2", ] [[package]] @@ -1486,7 +1486,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" dependencies = [ "equivalent", - "hashbrown 0.14.1", + "hashbrown 0.14.2", ] [[package]] From 502413aecbc9e9c24d24aa926bbfe6acb41183d3 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Thu, 19 Oct 2023 23:40:08 -0400 Subject: [PATCH 066/108] Fix martin integration tests (#952) Turns out some integration tests were not running after `martin` was moved to subdir --- Cargo.lock | 1 + Cargo.toml | 2 +- justfile | 9 +- martin-mbtiles/Cargo.toml | 2 +- martin/Cargo.toml | 1 + martin/src/srv/mod.rs | 2 +- martin/src/srv/server.rs | 23 ++- {tests => martin/tests}/mb_server_test.rs | 69 +++++-- .../tests}/pg_function_source_test.rs | 35 ++-- {tests => martin/tests}/pg_server_test.rs | 193 ++++++++++++++---- martin/tests/pg_table_source_test.rs | 185 +++++++++++++++++ {tests => martin/tests}/pmt_server_test.rs | 37 ++-- {tests => martin/tests}/utils/mod.rs | 8 +- {tests => martin/tests}/utils/pg_utils.rs | 6 +- tests/pg_table_source_test.rs | 117 ----------- 15 files changed, 456 insertions(+), 234 deletions(-) rename {tests => martin/tests}/mb_server_test.rs (82%) rename {tests => martin/tests}/pg_function_source_test.rs (68%) rename {tests => martin/tests}/pg_server_test.rs (85%) create mode 100644 martin/tests/pg_table_source_test.rs rename {tests => martin/tests}/pmt_server_test.rs (79%) rename {tests => martin/tests}/utils/mod.rs (81%) rename {tests => martin/tests}/utils/pg_utils.rs (89%) delete mode 100644 tests/pg_table_source_test.rs diff --git a/Cargo.lock b/Cargo.lock index ae55c4c0b..24a144281 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1710,6 +1710,7 @@ dependencies = [ "flate2", "futures", "indoc", + "insta", "itertools 0.11.0", "json-patch", "log", diff --git a/Cargo.toml b/Cargo.toml index ad632147f..a7ce09837 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ env_logger = "0.10" flate2 = "1" futures = "0.3" indoc = "2" -insta = { version = "1", features = ["toml"] } +insta = "1" itertools = "0.11" json-patch = "1.2" log = "0.4" diff --git a/justfile b/justfile index 2a78b4952..774a88746 100644 --- a/justfile +++ b/justfile @@ -2,6 +2,7 @@ set shell := ["bash", "-c"] +#export DATABASE_URL="postgres://postgres:postgres@localhost:5411/db" export PGPORT := "5411" export DATABASE_URL := "postgres://postgres:postgres@localhost:" + PGPORT + "/db" export CARGO_TERM_COLOR := "always" @@ -138,7 +139,7 @@ test-int: clean-test install-sqlx fi # Run integration tests and save its output as the new expected output -bless: restart clean-test bless-insta +bless: restart clean-test bless-insta-martin bless-insta-mbtiles rm -rf tests/temp cargo test -p martin --features bless-tests tests/test.sh @@ -146,10 +147,14 @@ bless: restart clean-test bless-insta mv tests/output tests/expected # Run integration tests and save its output as the new expected output -bless-insta *ARGS: (cargo-install "insta" "cargo-insta") +bless-insta-mbtiles *ARGS: (cargo-install "insta" "cargo-insta") #rm -rf martin-mbtiles/tests/snapshots cargo insta test --accept --unreferenced=auto -p martin-mbtiles {{ ARGS }} +# Run integration tests and save its output as the new expected output +bless-insta-martin *ARGS: (cargo-install "insta" "cargo-insta") + cargo insta test --accept --unreferenced=auto -p martin {{ ARGS }} + # Build and open mdbook documentation book: (cargo-install "mdbook") mdbook serve docs --open --port 8321 diff --git a/martin-mbtiles/Cargo.toml b/martin-mbtiles/Cargo.toml index 957618f4d..ef100d7fa 100644 --- a/martin-mbtiles/Cargo.toml +++ b/martin-mbtiles/Cargo.toml @@ -37,7 +37,7 @@ tokio = { workspace = true, features = ["rt-multi-thread"], optional = true } actix-rt.workspace = true ctor.workspace = true env_logger.workspace = true -insta.workspace = true +insta = { workspace = true, features = ["toml"] } pretty_assertions.workspace = true rstest.workspace = true diff --git a/martin/Cargo.toml b/martin/Cargo.toml index c1ca06366..38681e00b 100644 --- a/martin/Cargo.toml +++ b/martin/Cargo.toml @@ -92,6 +92,7 @@ cargo-husky.workspace = true criterion.workspace = true ctor.workspace = true indoc.workspace = true +insta = { workspace = true, features = ["yaml"] } #test-log = "0.2" [[bench]] diff --git a/martin/src/srv/mod.rs b/martin/src/srv/mod.rs index c637ca755..6b8e739e6 100644 --- a/martin/src/srv/mod.rs +++ b/martin/src/srv/mod.rs @@ -2,6 +2,6 @@ mod config; mod server; pub use config::{SrvConfig, KEEP_ALIVE_DEFAULT, LISTEN_ADDRESSES_DEFAULT}; -pub use server::{new_server, router, RESERVED_KEYWORDS}; +pub use server::{new_server, router, Catalog, RESERVED_KEYWORDS}; pub use crate::source::CatalogSourceEntry; diff --git a/martin/src/srv/server.rs b/martin/src/srv/server.rs index f058a72c0..7dc7ee99f 100755 --- a/martin/src/srv/server.rs +++ b/martin/src/srv/server.rs @@ -29,7 +29,7 @@ use crate::sprites::{SpriteCatalog, SpriteError, SpriteSources}; use crate::srv::config::{SrvConfig, KEEP_ALIVE_DEFAULT, LISTEN_ADDRESSES_DEFAULT}; use crate::utils::{decode_brotli, decode_gzip, encode_brotli, encode_gzip}; use crate::Error::BindingError; -use crate::Xyz; +use crate::{Error, Xyz}; /// List of keywords that cannot be used as source IDs. Some of these are reserved for future use. /// Reserved keywords must never end in a "dot number" (e.g. ".1"). @@ -51,6 +51,15 @@ pub struct Catalog { pub sprites: SpriteCatalog, } +impl Catalog { + pub fn new(state: &ServerState) -> Result { + Ok(Self { + tiles: state.tiles.get_catalog(), + sprites: state.sprites.get_catalog()?, + }) + } +} + #[derive(Deserialize)] struct TileJsonRequest { source_ids: String, @@ -421,26 +430,22 @@ pub fn router(cfg: &mut web::ServiceConfig) { } /// Create a new initialized Actix `App` instance together with the listening address. -pub fn new_server(config: SrvConfig, all_sources: ServerState) -> crate::Result<(Server, String)> { +pub fn new_server(config: SrvConfig, state: ServerState) -> crate::Result<(Server, String)> { + let catalog = Catalog::new(&state)?; let keep_alive = Duration::from_secs(config.keep_alive.unwrap_or(KEEP_ALIVE_DEFAULT)); let worker_processes = config.worker_processes.unwrap_or_else(num_cpus::get); let listen_addresses = config .listen_addresses .unwrap_or_else(|| LISTEN_ADDRESSES_DEFAULT.to_owned()); - let catalog = Catalog { - tiles: all_sources.tiles.get_catalog(), - sprites: all_sources.sprites.get_catalog()?, - }; - let server = HttpServer::new(move || { let cors_middleware = Cors::default() .allow_any_origin() .allowed_methods(vec!["GET"]); App::new() - .app_data(Data::new(all_sources.tiles.clone())) - .app_data(Data::new(all_sources.sprites.clone())) + .app_data(Data::new(state.tiles.clone())) + .app_data(Data::new(state.sprites.clone())) .app_data(Data::new(catalog.clone())) .wrap(cors_middleware) .wrap(middleware::NormalizePath::new(TrailingSlash::MergeOnly)) diff --git a/tests/mb_server_test.rs b/martin/tests/mb_server_test.rs similarity index 82% rename from tests/mb_server_test.rs rename to martin/tests/mb_server_test.rs index 94e1d1dff..31f3177ef 100644 --- a/tests/mb_server_test.rs +++ b/martin/tests/mb_server_test.rs @@ -2,8 +2,8 @@ use actix_web::http::header::{ACCEPT_ENCODING, CONTENT_ENCODING, CONTENT_TYPE}; use actix_web::test::{call_service, read_body, read_body_json, TestRequest}; use ctor::ctor; use indoc::indoc; +use insta::assert_yaml_snapshot; use martin::decode_gzip; -use martin::srv::IndexEntry; use tilejson::TileJSON; pub mod utils; @@ -16,11 +16,13 @@ fn init() { macro_rules! create_app { ($sources:expr) => {{ - let sources = mock_sources(mock_cfg($sources)).await.0; - let state = crate::utils::mock_app_data(sources).await; + let state = mock_sources(mock_cfg($sources)).await.0; ::actix_web::test::init_service( ::actix_web::App::new() - .app_data(state) + .app_data(actix_web::web::Data::new( + ::martin::srv::Catalog::new(&state).unwrap(), + )) + .app_data(actix_web::web::Data::new(state.tiles)) .configure(::martin::srv::router), ) .await @@ -34,10 +36,10 @@ fn test_get(path: &str) -> TestRequest { const CONFIG: &str = indoc! {" mbtiles: sources: - m_json: tests/fixtures/mbtiles/json.mbtiles - m_mvt: tests/fixtures/mbtiles/world_cities.mbtiles - m_raw_mvt: tests/fixtures/mbtiles/uncompressed_mvt.mbtiles - m_webp: tests/fixtures/mbtiles/webp.mbtiles + m_json: ../tests/fixtures/mbtiles/json.mbtiles + m_mvt: ../tests/fixtures/mbtiles/world_cities.mbtiles + m_raw_mvt: ../tests/fixtures/mbtiles/uncompressed_mvt.mbtiles + m_webp: ../tests/fixtures/mbtiles/webp.mbtiles "}; #[actix_rt::test] @@ -47,11 +49,27 @@ async fn mbt_get_catalog() { let req = test_get("/catalog").to_request(); let response = call_service(&app, req).await; assert!(response.status().is_success()); - let body = read_body(response).await; - let sources: Vec = serde_json::from_slice(&body).unwrap(); - assert_eq!(sources.iter().filter(|v| v.id == "m_mvt").count(), 1); - assert_eq!(sources.iter().filter(|v| v.id == "m_webp").count(), 1); - assert_eq!(sources.iter().filter(|v| v.id == "m_raw_mvt").count(), 1); + let body: serde_json::Value = read_body_json(response).await; + assert_yaml_snapshot!(body, @r###" + --- + tiles: + m_json: + content_type: application/json + name: Dummy json data + m_mvt: + content_type: application/x-protobuf + content_encoding: gzip + name: Major cities from Natural Earth data + description: Major cities from Natural Earth data + m_raw_mvt: + content_type: application/x-protobuf + name: Major cities from Natural Earth data + description: Major cities from Natural Earth data + m_webp: + content_type: image/webp + name: ne2sr + sprites: {} + "###); } #[actix_rt::test] @@ -62,10 +80,27 @@ async fn mbt_get_catalog_gzip() { let response = call_service(&app, req).await; assert!(response.status().is_success()); let body = decode_gzip(&read_body(response).await).unwrap(); - let sources: Vec = serde_json::from_slice(&body).unwrap(); - assert_eq!(sources.iter().filter(|v| v.id == "m_mvt").count(), 1); - assert_eq!(sources.iter().filter(|v| v.id == "m_webp").count(), 1); - assert_eq!(sources.iter().filter(|v| v.id == "m_raw_mvt").count(), 1); + let body: serde_json::Value = serde_json::from_slice(&body).unwrap(); + assert_yaml_snapshot!(body, @r###" + --- + tiles: + m_json: + content_type: application/json + name: Dummy json data + m_mvt: + content_type: application/x-protobuf + content_encoding: gzip + name: Major cities from Natural Earth data + description: Major cities from Natural Earth data + m_raw_mvt: + content_type: application/x-protobuf + name: Major cities from Natural Earth data + description: Major cities from Natural Earth data + m_webp: + content_type: image/webp + name: ne2sr + sprites: {} + "###); } #[actix_rt::test] diff --git a/tests/pg_function_source_test.rs b/martin/tests/pg_function_source_test.rs similarity index 68% rename from tests/pg_function_source_test.rs rename to martin/tests/pg_function_source_test.rs index 2db3a3791..b0940a109 100644 --- a/tests/pg_function_source_test.rs +++ b/martin/tests/pg_function_source_test.rs @@ -1,6 +1,6 @@ use ctor::ctor; use indoc::indoc; -use itertools::Itertools; +use insta::assert_yaml_snapshot; use martin::Xyz; pub mod utils; @@ -14,18 +14,15 @@ fn init() { #[actix_rt::test] async fn function_source_tilejson() { let mock = mock_sources(mock_pgcfg("connection_string: $DATABASE_URL")).await; - assert_eq!( - source(&mock, "function_zxy_query").get_tilejson(), - serde_json::from_str(indoc! {r#" -{ - "name": "function_zxy_query", - "description": "public.function_zxy_query", - "tilejson": "3.0.0", - "tiles": [] -} - "#}) - .unwrap() - ); + let tj = source(&mock, "function_zxy_query").get_tilejson(); + assert_yaml_snapshot!(tj, @r###" + --- + tilejson: 3.0.0 + tiles: [] + name: function_zxy_query + foo: + bar: foo + "###); } #[actix_rt::test] @@ -55,9 +52,11 @@ async fn function_source_schemas() { functions: from_schemas: MixedCase "}); - let sources = mock_sources(cfg).await.0; - assert_eq!( - sources.keys().sorted().collect::>(), - vec!["function_Mixed_Name"], - ); + let sources = mock_sources(cfg).await.0.tiles; + assert_yaml_snapshot!(sources.get_catalog(), @r###" + --- + function_Mixed_Name: + content_type: application/x-protobuf + description: a function source with MixedCase name + "###); } diff --git a/tests/pg_server_test.rs b/martin/tests/pg_server_test.rs similarity index 85% rename from tests/pg_server_test.rs rename to martin/tests/pg_server_test.rs index 7f4037a15..c814f554d 100644 --- a/tests/pg_server_test.rs +++ b/martin/tests/pg_server_test.rs @@ -3,7 +3,7 @@ use actix_web::http::StatusCode; use actix_web::test::{call_and_read_body_json, call_service, read_body, TestRequest}; use ctor::ctor; use indoc::indoc; -use martin::srv::IndexEntry; +use insta::assert_yaml_snapshot; use martin::OneOrMany; use tilejson::TileJSON; @@ -18,11 +18,13 @@ fn init() { macro_rules! create_app { ($sources:expr) => {{ let cfg = mock_cfg(indoc::indoc!($sources)); - let sources = mock_sources(cfg).await.0; - let state = crate::utils::mock_app_data(sources).await; + let state = mock_sources(cfg).await.0; ::actix_web::test::init_service( ::actix_web::App::new() - .app_data(state) + .app_data(actix_web::web::Data::new( + ::martin::srv::Catalog::new(&state).unwrap(), + )) + .app_data(actix_web::web::Data::new(state.tiles)) .configure(::martin::srv::router), ) .await @@ -44,16 +46,76 @@ postgres: let response = call_service(&app, req).await; assert!(response.status().is_success()); let body = read_body(response).await; - let sources: Vec = serde_json::from_slice(&body).unwrap(); - - let expected = "table_source"; - assert_eq!(sources.iter().filter(|v| v.id == expected).count(), 1); - - let expected = "function_zxy_query"; - assert_eq!(sources.iter().filter(|v| v.id == expected).count(), 1); - - let expected = "function_zxy_query_jsonb"; - assert_eq!(sources.iter().filter(|v| v.id == expected).count(), 1); + let body: serde_json::Value = serde_json::from_slice(&body).unwrap(); + assert_yaml_snapshot!(body, @r###" + --- + tiles: + MixPoints: + content_type: application/x-protobuf + description: a description from comment on table + auto_table: + content_type: application/x-protobuf + description: autodetect.auto_table.geom + bigint_table: + content_type: application/x-protobuf + description: autodetect.bigint_table.geom + function_Mixed_Name: + content_type: application/x-protobuf + description: a function source with MixedCase name + function_null: + content_type: application/x-protobuf + description: public.function_null + function_null_row: + content_type: application/x-protobuf + description: public.function_null_row + function_null_row2: + content_type: application/x-protobuf + description: public.function_null_row2 + function_zoom_xy: + content_type: application/x-protobuf + description: public.function_zoom_xy + function_zxy: + content_type: application/x-protobuf + description: public.function_zxy + function_zxy2: + content_type: application/x-protobuf + description: public.function_zxy2 + function_zxy_query: + content_type: application/x-protobuf + function_zxy_query_jsonb: + content_type: application/x-protobuf + description: public.function_zxy_query_jsonb + function_zxy_query_test: + content_type: application/x-protobuf + description: public.function_zxy_query_test + function_zxy_row: + content_type: application/x-protobuf + description: public.function_zxy_row + function_zxy_row_key: + content_type: application/x-protobuf + description: public.function_zxy_row_key + points1: + content_type: application/x-protobuf + description: public.points1.geom + points1_vw: + content_type: application/x-protobuf + description: public.points1_vw.geom + points2: + content_type: application/x-protobuf + description: public.points2.geom + points3857: + content_type: application/x-protobuf + description: public.points3857.geom + table_source: + content_type: application/x-protobuf + table_source_multiple_geom: + content_type: application/x-protobuf + description: public.table_source_multiple_geom.geom1 + table_source_multiple_geom.1: + content_type: application/x-protobuf + description: public.table_source_multiple_geom.geom2 + sprites: {} + "###); } #[actix_rt::test] @@ -947,44 +1009,85 @@ tables: let src = table(&mock, "no_id"); assert_eq!(src.id_column, None); assert!(matches!(&src.properties, Some(v) if v.len() == 1)); - assert_eq!( - source(&mock, "no_id").get_tilejson(), - serde_json::from_str(indoc! {r#" -{ - "name": "no_id", - "description": "MixedCase.MixPoints.Geom", - "tilejson": "3.0.0", - "tiles": [], - "vector_layers": [ - { - "id": "no_id", - "fields": {"TABLE": "text"} - } - ], - "bounds": [-180.0, -90.0, 180.0, 90.0] -} - "#}) - .unwrap() - ); - - let src = table(&mock, "id_only"); - assert_eq!(src.id_column, some("giD")); - assert!(matches!(&src.properties, Some(v) if v.len() == 1)); + let tj = source(&mock, "no_id").get_tilejson(); + assert_yaml_snapshot!(tj, @r###" + --- + tilejson: 3.0.0 + tiles: [] + vector_layers: + - id: no_id + fields: + TABLE: text + bounds: + - -180 + - -90 + - 180 + - 90 + description: MixedCase.MixPoints.Geom + name: no_id + "###); + + assert_yaml_snapshot!(table(&mock, "id_only"), @r###" + --- + schema: MixedCase + table: MixPoints + srid: 4326 + geometry_column: Geom + id_column: giD + bounds: + - -180 + - -90 + - 180 + - 90 + geometry_type: POINT + properties: + TABLE: text + "###); - let src = table(&mock, "id_and_prop"); - assert_eq!(src.id_column, some("giD")); - assert!(matches!(&src.properties, Some(v) if v.len() == 2)); + assert_yaml_snapshot!(table(&mock, "id_and_prop"), @r###" + --- + schema: MixedCase + table: MixPoints + srid: 4326 + geometry_column: Geom + id_column: giD + bounds: + - -180 + - -90 + - 180 + - 90 + geometry_type: POINT + properties: + TABLE: text + giD: int4 + "###); - let src = table(&mock, "prop_only"); - assert_eq!(src.id_column, None); - assert!(matches!(&src.properties, Some(v) if v.len() == 2)); + assert_yaml_snapshot!(table(&mock, "prop_only"), @r###" + --- + schema: MixedCase + table: MixPoints + srid: 4326 + geometry_column: Geom + bounds: + - -180 + - -90 + - 180 + - 90 + geometry_type: POINT + properties: + TABLE: text + giD: int4 + "###); // -------------------------------------------- - let state = mock_app_data(mock.0).await; + let state = mock_sources(cfg.clone()).await.0; let app = ::actix_web::test::init_service( ::actix_web::App::new() - .app_data(state) + .app_data(actix_web::web::Data::new( + ::martin::srv::Catalog::new(&state).unwrap(), + )) + .app_data(actix_web::web::Data::new(state.tiles)) .configure(::martin::srv::router), ) .await; diff --git a/martin/tests/pg_table_source_test.rs b/martin/tests/pg_table_source_test.rs new file mode 100644 index 000000000..6b1f28a14 --- /dev/null +++ b/martin/tests/pg_table_source_test.rs @@ -0,0 +1,185 @@ +use ctor::ctor; +use indoc::indoc; +use insta::assert_yaml_snapshot; +use martin::Xyz; + +pub mod utils; +pub use utils::*; + +#[ctor] +fn init() { + let _ = env_logger::builder().is_test(true).try_init(); +} + +#[actix_rt::test] +async fn table_source() { + let mock = mock_sources(mock_pgcfg("connection_string: $DATABASE_URL")).await; + assert_yaml_snapshot!(mock.0.tiles.get_catalog(), @r###" + --- + MixPoints: + content_type: application/x-protobuf + description: a description from comment on table + auto_table: + content_type: application/x-protobuf + description: autodetect.auto_table.geom + bigint_table: + content_type: application/x-protobuf + description: autodetect.bigint_table.geom + function_Mixed_Name: + content_type: application/x-protobuf + description: a function source with MixedCase name + function_null: + content_type: application/x-protobuf + description: public.function_null + function_null_row: + content_type: application/x-protobuf + description: public.function_null_row + function_null_row2: + content_type: application/x-protobuf + description: public.function_null_row2 + function_zoom_xy: + content_type: application/x-protobuf + description: public.function_zoom_xy + function_zxy: + content_type: application/x-protobuf + description: public.function_zxy + function_zxy2: + content_type: application/x-protobuf + description: public.function_zxy2 + function_zxy_query: + content_type: application/x-protobuf + function_zxy_query_jsonb: + content_type: application/x-protobuf + description: public.function_zxy_query_jsonb + function_zxy_query_test: + content_type: application/x-protobuf + description: public.function_zxy_query_test + function_zxy_row: + content_type: application/x-protobuf + description: public.function_zxy_row + function_zxy_row_key: + content_type: application/x-protobuf + description: public.function_zxy_row_key + points1: + content_type: application/x-protobuf + description: public.points1.geom + points1_vw: + content_type: application/x-protobuf + description: public.points1_vw.geom + points2: + content_type: application/x-protobuf + description: public.points2.geom + points3857: + content_type: application/x-protobuf + description: public.points3857.geom + table_source: + content_type: application/x-protobuf + table_source_multiple_geom: + content_type: application/x-protobuf + description: public.table_source_multiple_geom.geom1 + table_source_multiple_geom.1: + content_type: application/x-protobuf + description: public.table_source_multiple_geom.geom2 + "###); + + let source = table(&mock, "table_source"); + assert_yaml_snapshot!(source, @r###" + --- + schema: public + table: table_source + srid: 4326 + geometry_column: geom + bounds: + - -2 + - -1 + - 142.84131509869133 + - 45 + geometry_type: GEOMETRY + properties: + gid: int4 + "###); +} + +#[actix_rt::test] +async fn tables_tilejson() { + let mock = mock_sources(mock_pgcfg("connection_string: $DATABASE_URL")).await; + let tj = source(&mock, "table_source").get_tilejson(); + assert_yaml_snapshot!(tj, @r###" + --- + tilejson: 3.0.0 + tiles: [] + vector_layers: + - id: table_source + fields: + gid: int4 + bounds: + - -2 + - -1 + - 142.84131509869133 + - 45 + name: table_source + foo: + bar: foo + "###); +} + +#[actix_rt::test] +async fn tables_tile_ok() { + let mock = mock_sources(mock_pgcfg("connection_string: $DATABASE_URL")).await; + let tile = source(&mock, "table_source") + .get_tile(&Xyz { z: 0, x: 0, y: 0 }, &None) + .await + .unwrap(); + + assert!(!tile.is_empty()); +} + +#[actix_rt::test] +async fn tables_srid_ok() { + let mock = mock_sources(mock_pgcfg(indoc! {" + connection_string: $DATABASE_URL + default_srid: 900913 + "})) + .await; + + let source = table(&mock, "points1"); + assert_eq!(source.srid, 4326); + + let source = table(&mock, "points2"); + assert_eq!(source.srid, 4326); + + let source = table(&mock, "points3857"); + assert_eq!(source.srid, 3857); + + let source = table(&mock, "points_empty_srid"); + assert_eq!(source.srid, 900_913); +} + +#[actix_rt::test] +async fn tables_multiple_geom_ok() { + let mock = mock_sources(mock_pgcfg("connection_string: $DATABASE_URL")).await; + + let source = table(&mock, "table_source_multiple_geom"); + assert_eq!(source.geometry_column, "geom1"); + + let source = table(&mock, "table_source_multiple_geom.1"); + assert_eq!(source.geometry_column, "geom2"); +} + +#[actix_rt::test] +async fn table_source_schemas() { + let cfg = mock_pgcfg(indoc! {" + connection_string: $DATABASE_URL + auto_publish: + tables: + from_schemas: MixedCase + functions: false + "}); + let sources = mock_sources(cfg).await.0; + assert_yaml_snapshot!(sources.tiles.get_catalog(), @r###" + --- + MixPoints: + content_type: application/x-protobuf + description: a description from comment on table + "###); +} diff --git a/tests/pmt_server_test.rs b/martin/tests/pmt_server_test.rs similarity index 79% rename from tests/pmt_server_test.rs rename to martin/tests/pmt_server_test.rs index 916601a87..3ed778313 100644 --- a/tests/pmt_server_test.rs +++ b/martin/tests/pmt_server_test.rs @@ -2,8 +2,8 @@ use actix_web::http::header::{ACCEPT_ENCODING, CONTENT_ENCODING, CONTENT_TYPE}; use actix_web::test::{call_service, read_body, read_body_json, TestRequest}; use ctor::ctor; use indoc::indoc; +use insta::assert_yaml_snapshot; use martin::decode_gzip; -use martin::srv::IndexEntry; use tilejson::TileJSON; pub mod utils; @@ -16,11 +16,13 @@ fn init() { macro_rules! create_app { ($sources:expr) => {{ - let sources = mock_sources(mock_cfg($sources)).await.0; - let state = crate::utils::mock_app_data(sources).await; + let state = mock_sources(mock_cfg($sources)).await.0; ::actix_web::test::init_service( ::actix_web::App::new() - .app_data(state) + .app_data(actix_web::web::Data::new( + ::martin::srv::Catalog::new(&state).unwrap(), + )) + .app_data(actix_web::web::Data::new(state.tiles)) .configure(::martin::srv::router), ) .await @@ -34,22 +36,25 @@ fn test_get(path: &str) -> TestRequest { const CONFIG: &str = indoc! {" pmtiles: sources: - p_png: tests/fixtures/pmtiles/stamen_toner__raster_CC-BY+ODbL_z3.pmtiles + p_png: ../tests/fixtures/pmtiles/stamen_toner__raster_CC-BY+ODbL_z3.pmtiles "}; #[actix_rt::test] async fn pmt_get_catalog() { - let path = "pmtiles: tests/fixtures/pmtiles/stamen_toner__raster_CC-BY+ODbL_z3.pmtiles"; + let path = "pmtiles: ../tests/fixtures/pmtiles/stamen_toner__raster_CC-BY+ODbL_z3.pmtiles"; let app = create_app! { path }; let req = test_get("/catalog").to_request(); let response = call_service(&app, req).await; assert!(response.status().is_success()); - let body = read_body(response).await; - let sources: Vec = serde_json::from_slice(&body).unwrap(); - - let expected = "stamen_toner__raster_CC-BY-ODbL_z3"; - assert_eq!(sources.iter().filter(|v| v.id == expected).count(), 1); + let body: serde_json::Value = read_body_json(response).await; + assert_yaml_snapshot!(body, @r###" + --- + tiles: + stamen_toner__raster_CC-BY-ODbL_z3: + content_type: image/png + sprites: {} + "###); } #[actix_rt::test] @@ -60,8 +65,14 @@ async fn pmt_get_catalog_gzip() { let response = call_service(&app, req).await; assert!(response.status().is_success()); let body = decode_gzip(&read_body(response).await).unwrap(); - let sources: Vec = serde_json::from_slice(&body).unwrap(); - assert_eq!(sources.iter().filter(|v| v.id == "p_png").count(), 1); + let body: serde_json::Value = serde_json::from_slice(&body).unwrap(); + assert_yaml_snapshot!(body, @r###" + --- + tiles: + p_png: + content_type: image/png + sprites: {} + "###); } #[actix_rt::test] diff --git a/tests/utils/mod.rs b/martin/tests/utils/mod.rs similarity index 81% rename from tests/utils/mod.rs rename to martin/tests/utils/mod.rs index d15588ea7..e61dae3d0 100644 --- a/tests/utils/mod.rs +++ b/martin/tests/utils/mod.rs @@ -4,10 +4,8 @@ mod pg_utils; -use actix_web::web::Data; use log::warn; -use martin::srv::AppState; -use martin::{Config, Sources}; +use martin::Config; pub use pg_utils::*; #[path = "../../src/utils/test_utils.rs"] @@ -15,10 +13,6 @@ mod test_utils; #[allow(clippy::wildcard_imports)] pub use test_utils::*; -pub async fn mock_app_data(sources: Sources) -> Data { - Data::new(sources) -} - #[must_use] pub fn mock_cfg(yaml: &str) -> Config { let env = if let Ok(db_url) = std::env::var("DATABASE_URL") { diff --git a/tests/utils/pg_utils.rs b/martin/tests/utils/pg_utils.rs similarity index 89% rename from tests/utils/pg_utils.rs rename to martin/tests/utils/pg_utils.rs index b5c729d34..da5da7d3d 100644 --- a/tests/utils/pg_utils.rs +++ b/martin/tests/utils/pg_utils.rs @@ -1,7 +1,7 @@ use indoc::formatdoc; pub use martin::args::Env; use martin::pg::TableInfo; -use martin::{Config, IdResolver, Source, Sources}; +use martin::{Config, IdResolver, ServerState, Source}; use crate::mock_cfg; @@ -10,7 +10,7 @@ use crate::mock_cfg; // Each function should allow dead_code as they might not be used by a specific test file. // -pub type MockSource = (Sources, Config); +pub type MockSource = (ServerState, Config); #[allow(dead_code)] #[must_use] @@ -48,5 +48,5 @@ pub fn table<'a>(mock: &'a MockSource, name: &str) -> &'a TableInfo { #[must_use] pub fn source<'a>(mock: &'a MockSource, name: &str) -> &'a dyn Source { let (sources, _) = mock; - sources.get(name).unwrap().as_ref() + sources.tiles.get_source(name).unwrap() } diff --git a/tests/pg_table_source_test.rs b/tests/pg_table_source_test.rs deleted file mode 100644 index 5bba0b85b..000000000 --- a/tests/pg_table_source_test.rs +++ /dev/null @@ -1,117 +0,0 @@ -use std::collections::HashMap; - -use ctor::ctor; -use indoc::indoc; -use martin::Xyz; - -pub mod utils; -pub use utils::*; - -#[ctor] -fn init() { - let _ = env_logger::builder().is_test(true).try_init(); -} - -#[actix_rt::test] -async fn table_source() { - let mock = mock_sources(mock_pgcfg("connection_string: $DATABASE_URL")).await; - assert!(!mock.0.is_empty()); - - let source = table(&mock, "table_source"); - assert_eq!(source.schema, "public"); - assert_eq!(source.table, "table_source"); - assert_eq!(source.srid, 4326); - assert_eq!(source.geometry_column, "geom"); - assert_eq!(source.id_column, None); - assert_eq!(source.minzoom, None); - assert_eq!(source.maxzoom, None); - assert!(source.bounds.is_some()); - assert_eq!(source.extent, Some(4096)); - assert_eq!(source.buffer, Some(64)); - assert_eq!(source.clip_geom, Some(true)); - assert_eq!(source.geometry_type, some("GEOMETRY")); - - let mut properties = HashMap::new(); - properties.insert("gid".to_owned(), "int4".to_owned()); - assert_eq!(source.properties, Some(properties)); -} - -#[actix_rt::test] -async fn tables_tilejson() { - let mock = mock_sources(mock_pgcfg("connection_string: $DATABASE_URL")).await; - assert_eq!( - source(&mock, "table_source").get_tilejson(), - serde_json::from_str(indoc! {r#" -{ - "name": "table_source", - "description": "public.table_source.geom", - "tilejson": "3.0.0", - "tiles": [], - "vector_layers": [ - { - "id": "table_source", - "fields": {"gid": "int4"} - } - ], - "bounds": [-2.0, -1.0, 142.84131509869133, 45.0] -} - "#}) - .unwrap() - ); -} - -#[actix_rt::test] -async fn tables_tile_ok() { - let mock = mock_sources(mock_pgcfg("connection_string: $DATABASE_URL")).await; - let tile = source(&mock, "table_source") - .get_tile(&Xyz { z: 0, x: 0, y: 0 }, &None) - .await - .unwrap(); - - assert!(!tile.is_empty()); -} - -#[actix_rt::test] -async fn tables_srid_ok() { - let mock = mock_sources(mock_pgcfg(indoc! {" - connection_string: $DATABASE_URL - default_srid: 900913 - "})) - .await; - - let source = table(&mock, "points1"); - assert_eq!(source.srid, 4326); - - let source = table(&mock, "points2"); - assert_eq!(source.srid, 4326); - - let source = table(&mock, "points3857"); - assert_eq!(source.srid, 3857); - - let source = table(&mock, "points_empty_srid"); - assert_eq!(source.srid, 900_913); -} - -#[actix_rt::test] -async fn tables_multiple_geom_ok() { - let mock = mock_sources(mock_pgcfg("connection_string: $DATABASE_URL")).await; - - let source = table(&mock, "table_source_multiple_geom"); - assert_eq!(source.geometry_column, "geom1"); - - let source = table(&mock, "table_source_multiple_geom.1"); - assert_eq!(source.geometry_column, "geom2"); -} - -#[actix_rt::test] -async fn table_source_schemas() { - let cfg = mock_pgcfg(indoc! {" - connection_string: $DATABASE_URL - auto_publish: - tables: - from_schemas: MixedCase - functions: false - "}); - let sources = mock_sources(cfg).await.0; - assert_eq!(sources.keys().collect::>(), vec!["MixPoints"]); -} From 196df9e8062f0f7a66c78ffd0f15113dd70af78d Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Fri, 20 Oct 2023 14:31:43 -0400 Subject: [PATCH 067/108] miro refactor and lock bump --- Cargo.lock | 4 ++-- martin/src/srv/server.rs | 15 ++++++++------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 24a144281..0ae418ec7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -665,9 +665,9 @@ checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" [[package]] name = "cpufeatures" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +checksum = "3fbc60abd742b35f2492f808e1abbb83d45f72db402e14c55057edc9c7b1e9e4" dependencies = [ "libc", ] diff --git a/martin/src/srv/server.rs b/martin/src/srv/server.rs index 7dc7ee99f..6630f4270 100755 --- a/martin/src/srv/server.rs +++ b/martin/src/srv/server.rs @@ -160,19 +160,20 @@ async fn git_source_info( sources: Data, ) -> Result { let sources = sources.get_sources(&path.source_ids, None)?.0; - - let tiles_path = req - .headers() - .get("x-rewrite-url") - .and_then(parse_x_rewrite_url) - .unwrap_or_else(|| req.path().to_owned()); - let info = req.connection_info(); + let tiles_path = get_request_path(&req); let tiles_url = get_tiles_url(info.scheme(), info.host(), req.query_string(), &tiles_path)?; Ok(HttpResponse::Ok().json(merge_tilejson(sources, tiles_url))) } +fn get_request_path(req: &HttpRequest) -> String { + req.headers() + .get("x-rewrite-url") + .and_then(parse_x_rewrite_url) + .unwrap_or_else(|| req.path().to_owned()) +} + fn get_tiles_url(scheme: &str, host: &str, query_string: &str, tiles_path: &str) -> Result { let path_and_query = if query_string.is_empty() { format!("{tiles_path}/{{z}}/{{x}}/{{y}}") From e377bd62acd42ff250a09d0a40e7eeb864139833 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Sat, 21 Oct 2023 14:11:02 -0400 Subject: [PATCH 068/108] Simplify conf parsing, separate func & tbl configs (#956) * Simplify code by adding `None` to the enums we use for configuration * Separate postgres auto-publish configuration into table and function structs --- Cargo.lock | 4 +- martin/src/args/pg.rs | 26 +- martin/src/args/root.rs | 16 +- martin/src/config.rs | 70 ++--- martin/src/file_config.rs | 177 ++++++------ martin/src/lib.rs | 2 +- martin/src/pg/config.rs | 60 ++-- martin/src/pg/configurator.rs | 424 +++++++++++++++++------------ martin/src/pg/mod.rs | 4 +- martin/src/sprites/mod.rs | 27 +- martin/src/utils/cfg_containers.rs | 143 ++++++++++ martin/src/utils/mod.rs | 4 +- martin/src/utils/one_or_many.rs | 95 ------- martin/src/utils/utilities.rs | 25 +- martin/tests/pg_server_test.rs | 4 +- martin/tests/utils/pg_utils.rs | 2 - 16 files changed, 596 insertions(+), 487 deletions(-) mode change 100755 => 100644 martin/src/pg/configurator.rs create mode 100644 martin/src/utils/cfg_containers.rs delete mode 100644 martin/src/utils/one_or_many.rs diff --git a/Cargo.lock b/Cargo.lock index 0ae418ec7..75ed2c211 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2842,9 +2842,9 @@ checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" [[package]] name = "socket2" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" dependencies = [ "libc", "windows-sys 0.48.0", diff --git a/martin/src/args/pg.rs b/martin/src/args/pg.rs index f77c06716..0ca12136e 100644 --- a/martin/src/args/pg.rs +++ b/martin/src/args/pg.rs @@ -4,7 +4,7 @@ use crate::args::connections::Arguments; use crate::args::connections::State::{Ignore, Take}; use crate::args::environment::Env; use crate::pg::{PgConfig, PgSslCerts, POOL_SIZE_DEFAULT}; -use crate::utils::OneOrMany; +use crate::utils::{OptBoolObj, OptOneMany}; #[derive(clap::Args, Debug, PartialEq, Default)] #[command(about, version)] @@ -30,7 +30,7 @@ impl PgArgs { self, cli_strings: &mut Arguments, env: &impl Env<'a>, - ) -> Option> { + ) -> OptOneMany { let connections = Self::extract_conn_strings(cli_strings, env); let default_srid = self.get_default_srid(env); let certs = self.get_certs(env); @@ -48,20 +48,20 @@ impl PgArgs { }, max_feature_count: self.max_feature_count, pool_size: self.pool_size, - auto_publish: None, + auto_publish: OptBoolObj::NoValue, tables: None, functions: None, }) .collect(); match results.len() { - 0 => None, - 1 => Some(OneOrMany::One(results.into_iter().next().unwrap())), - _ => Some(OneOrMany::Many(results)), + 0 => OptOneMany::NoVals, + 1 => OptOneMany::One(results.into_iter().next().unwrap()), + _ => OptOneMany::Many(results), } } - pub fn override_config<'a>(self, pg_config: &mut OneOrMany, env: &impl Env<'a>) { + pub fn override_config<'a>(self, pg_config: &mut OptOneMany, env: &impl Env<'a>) { if self.default_srid.is_some() { info!("Overriding configured default SRID to {} on all Postgres connections because of a CLI parameter", self.default_srid.unwrap()); pg_config.iter_mut().for_each(|c| { @@ -224,10 +224,10 @@ mod tests { let config = PgArgs::default().into_config(&mut args, &FauxEnv::default()); assert_eq!( config, - Some(OneOrMany::One(PgConfig { + OptOneMany::One(PgConfig { connection_string: some("postgres://localhost:5432"), ..Default::default() - })) + }) ); assert!(args.check().is_ok()); } @@ -248,7 +248,7 @@ mod tests { let config = PgArgs::default().into_config(&mut args, &env); assert_eq!( config, - Some(OneOrMany::One(PgConfig { + OptOneMany::One(PgConfig { connection_string: some("postgres://localhost:5432"), default_srid: Some(10), ssl_certificates: PgSslCerts { @@ -256,7 +256,7 @@ mod tests { ..Default::default() }, ..Default::default() - })) + }) ); assert!(args.check().is_ok()); } @@ -282,7 +282,7 @@ mod tests { let config = pg_args.into_config(&mut args, &env); assert_eq!( config, - Some(OneOrMany::One(PgConfig { + OptOneMany::One(PgConfig { connection_string: some("postgres://localhost:5432"), default_srid: Some(20), ssl_certificates: PgSslCerts { @@ -291,7 +291,7 @@ mod tests { ssl_root_cert: Some(PathBuf::from("root")), }, ..Default::default() - })) + }) ); assert!(args.check().is_ok()); } diff --git a/martin/src/args/root.rs b/martin/src/args/root.rs index 1201279b9..79a4846de 100644 --- a/martin/src/args/root.rs +++ b/martin/src/args/root.rs @@ -62,11 +62,11 @@ impl Args { let mut cli_strings = Arguments::new(self.meta.connection); let pg_args = self.pg.unwrap_or_default(); - if let Some(pg_config) = &mut config.postgres { - // config was loaded from a file, we can only apply a few CLI overrides to it - pg_args.override_config(pg_config, env); - } else { + if config.postgres.is_none() { config.postgres = pg_args.into_config(&mut cli_strings, env); + } else { + // config was loaded from a file, we can only apply a few CLI overrides to it + pg_args.override_config(&mut config.postgres, env); } if !cli_strings.is_empty() { @@ -85,7 +85,7 @@ impl Args { } } -pub fn parse_file_args(cli_strings: &mut Arguments, extension: &str) -> Option { +pub fn parse_file_args(cli_strings: &mut Arguments, extension: &str) -> FileConfigEnum { let paths = cli_strings.process(|v| match PathBuf::try_from(v) { Ok(v) => { if v.is_dir() { @@ -107,7 +107,7 @@ mod tests { use super::*; use crate::pg::PgConfig; use crate::test_utils::{some, FauxEnv}; - use crate::utils::OneOrMany; + use crate::utils::OptOneMany; fn parse(args: &[&str]) -> Result<(Config, MetaArgs)> { let args = Args::parse_from(args); @@ -143,10 +143,10 @@ mod tests { let args = parse(&["martin", "postgres://connection"]).unwrap(); let cfg = Config { - postgres: Some(OneOrMany::One(PgConfig { + postgres: OptOneMany::One(PgConfig { connection_string: some("postgres://connection"), ..Default::default() - })), + }), ..Default::default() }; let meta = MetaArgs { diff --git a/martin/src/config.rs b/martin/src/config.rs index 5856bed23..90005d033 100644 --- a/martin/src/config.rs +++ b/martin/src/config.rs @@ -17,7 +17,7 @@ use crate::source::{TileInfoSources, TileSources}; use crate::sprites::SpriteSources; use crate::srv::SrvConfig; use crate::Error::{ConfigLoadError, ConfigParseError, NoSources}; -use crate::{IdResolver, OneOrMany, Result}; +use crate::{IdResolver, OptOneMany, Result}; pub type UnrecognizedValues = HashMap; @@ -31,17 +31,17 @@ pub struct Config { #[serde(flatten)] pub srv: SrvConfig, - #[serde(skip_serializing_if = "Option::is_none")] - pub postgres: Option>, + #[serde(default, skip_serializing_if = "OptOneMany::is_none")] + pub postgres: OptOneMany, - #[serde(skip_serializing_if = "Option::is_none")] - pub pmtiles: Option, + #[serde(default, skip_serializing_if = "FileConfigEnum::is_none")] + pub pmtiles: FileConfigEnum, - #[serde(skip_serializing_if = "Option::is_none")] - pub mbtiles: Option, + #[serde(default, skip_serializing_if = "FileConfigEnum::is_none")] + pub mbtiles: FileConfigEnum, - #[serde(skip_serializing_if = "Option::is_none")] - pub sprites: Option, + #[serde(default, skip_serializing_if = "FileConfigEnum::is_none")] + pub sprites: FileConfigEnum, #[serde(flatten)] pub unrecognized: UnrecognizedValues, @@ -53,40 +53,22 @@ impl Config { let mut res = UnrecognizedValues::new(); copy_unrecognized_config(&mut res, "", &self.unrecognized); - let mut any = if let Some(pg) = &mut self.postgres { - for pg in pg.iter_mut() { - res.extend(pg.finalize()?); - } - !pg.is_empty() - } else { - false - }; - - any |= if let Some(cfg) = &mut self.pmtiles { - res.extend(cfg.finalize("pmtiles.")?); - !cfg.is_empty() - } else { - false - }; + for pg in self.postgres.iter_mut() { + res.extend(pg.finalize()?); + } - any |= if let Some(cfg) = &mut self.mbtiles { - res.extend(cfg.finalize("mbtiles.")?); - !cfg.is_empty() - } else { - false - }; + res.extend(self.pmtiles.finalize("pmtiles.")?); + res.extend(self.mbtiles.finalize("mbtiles.")?); + res.extend(self.sprites.finalize("sprites.")?); - any |= if let Some(cfg) = &mut self.sprites { - res.extend(cfg.finalize("sprites.")?); - !cfg.is_empty() + if self.postgres.is_empty() + && self.pmtiles.is_empty() + && self.mbtiles.is_empty() + && self.sprites.is_empty() + { + Err(NoSources) } else { - false - }; - - if any { Ok(res) - } else { - Err(NoSources) } } @@ -102,18 +84,16 @@ impl Config { let create_mbt_src = &mut MbtSource::new_box; let mut sources: Vec>>>> = Vec::new(); - if let Some(v) = self.postgres.as_mut() { - for s in v.iter_mut() { - sources.push(Box::pin(s.resolve(idr.clone()))); - } + for s in self.postgres.iter_mut() { + sources.push(Box::pin(s.resolve(idr.clone()))); } - if self.pmtiles.is_some() { + if !self.pmtiles.is_empty() { let val = resolve_files(&mut self.pmtiles, idr.clone(), "pmtiles", create_pmt_src); sources.push(Box::pin(val)); } - if self.mbtiles.is_some() { + if !self.mbtiles.is_empty() { let val = resolve_files(&mut self.mbtiles, idr.clone(), "mbtiles", create_mbt_src); sources.push(Box::pin(val)); } diff --git a/martin/src/file_config.rs b/martin/src/file_config.rs index 9cb55a359..62d8ab30e 100644 --- a/martin/src/file_config.rs +++ b/martin/src/file_config.rs @@ -10,8 +10,8 @@ use serde::{Deserialize, Serialize}; use crate::config::{copy_unrecognized_config, UnrecognizedValues}; use crate::file_config::FileError::{InvalidFilePath, InvalidSourceFilePath, IoError}; use crate::source::{Source, TileInfoSources}; -use crate::utils::{sorted_opt_map, Error, IdResolver, OneOrMany}; -use crate::OneOrMany::{Many, One}; +use crate::utils::{sorted_opt_map, Error, IdResolver, OptOneMany}; +use crate::OptOneMany::{Many, One}; #[derive(thiserror::Error, Debug)] pub enum FileError { @@ -31,9 +31,11 @@ pub enum FileError { AquireConnError(String), } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] #[serde(untagged)] pub enum FileConfigEnum { + #[default] + None, Path(PathBuf), Paths(Vec), Config(FileConfig), @@ -41,7 +43,7 @@ pub enum FileConfigEnum { impl FileConfigEnum { #[must_use] - pub fn new(paths: Vec) -> Option { + pub fn new(paths: Vec) -> FileConfigEnum { Self::new_extended(paths, HashMap::new(), UnrecognizedValues::new()) } @@ -50,46 +52,70 @@ impl FileConfigEnum { paths: Vec, configs: HashMap, unrecognized: UnrecognizedValues, - ) -> Option { + ) -> FileConfigEnum { if configs.is_empty() && unrecognized.is_empty() { match paths.len() { - 0 => None, - 1 => Some(FileConfigEnum::Path(paths.into_iter().next().unwrap())), - _ => Some(FileConfigEnum::Paths(paths)), + 0 => FileConfigEnum::None, + 1 => FileConfigEnum::Path(paths.into_iter().next().unwrap()), + _ => FileConfigEnum::Paths(paths), } } else { - Some(FileConfigEnum::Config(FileConfig { - paths: OneOrMany::new_opt(paths), + FileConfigEnum::Config(FileConfig { + paths: OptOneMany::new(paths), sources: if configs.is_empty() { None } else { Some(configs) }, unrecognized, - })) + }) + } + } + + #[must_use] + pub fn is_none(&self) -> bool { + matches!(self, Self::None) + } + + #[must_use] + pub fn is_empty(&self) -> bool { + match self { + Self::None => true, + Self::Path(_) => false, + Self::Paths(v) => v.is_empty(), + Self::Config(c) => c.is_empty(), } } - pub fn extract_file_config(&mut self) -> FileConfig { + pub fn extract_file_config(&mut self) -> Option { match self { - FileConfigEnum::Path(path) => FileConfig { - paths: Some(One(mem::take(path))), + FileConfigEnum::None => None, + FileConfigEnum::Path(path) => Some(FileConfig { + paths: One(mem::take(path)), ..FileConfig::default() - }, - FileConfigEnum::Paths(paths) => FileConfig { - paths: Some(Many(mem::take(paths))), + }), + FileConfigEnum::Paths(paths) => Some(FileConfig { + paths: Many(mem::take(paths)), ..Default::default() - }, - FileConfigEnum::Config(cfg) => mem::take(cfg), + }), + FileConfigEnum::Config(cfg) => Some(mem::take(cfg)), } } + + pub fn finalize(&self, prefix: &str) -> Result { + let mut res = UnrecognizedValues::new(); + if let Self::Config(cfg) = self { + copy_unrecognized_config(&mut res, prefix, &cfg.unrecognized); + } + Ok(res) + } } #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] pub struct FileConfig { /// A list of file paths - #[serde(skip_serializing_if = "Option::is_none")] - pub paths: Option>, + #[serde(default, skip_serializing_if = "OptOneMany::is_none")] + pub paths: OptOneMany, /// A map of source IDs to file paths or config objects #[serde(skip_serializing_if = "Option::is_none")] #[serde(serialize_with = "sorted_opt_map")] @@ -128,27 +154,8 @@ pub struct FileConfigSource { pub path: PathBuf, } -impl FileConfigEnum { - pub fn finalize(&self, prefix: &str) -> Result { - let mut res = UnrecognizedValues::new(); - if let Self::Config(cfg) = self { - copy_unrecognized_config(&mut res, prefix, &cfg.unrecognized); - } - Ok(res) - } - - #[must_use] - pub fn is_empty(&self) -> bool { - match self { - Self::Path(_) => false, - Self::Paths(v) => v.is_empty(), - Self::Config(c) => c.is_empty(), - } - } -} - pub async fn resolve_files( - config: &mut Option, + config: &mut FileConfigEnum, idr: IdResolver, extension: &str, create_source: &mut impl FnMut(String, PathBuf) -> Fut, @@ -162,7 +169,7 @@ where } async fn resolve_int( - config: &mut Option, + config: &mut FileConfigEnum, idr: IdResolver, extension: &str, create_source: &mut impl FnMut(String, PathBuf) -> Fut, @@ -170,10 +177,9 @@ async fn resolve_int( where Fut: Future, FileError>>, { - let Some(cfg) = config else { + let Some(cfg) = config.extract_file_config() else { return Ok(TileInfoSources::default()); }; - let cfg = cfg.extract_file_config(); let mut results = TileInfoSources::default(); let mut configs = HashMap::new(); @@ -202,50 +208,47 @@ where } } - if let Some(paths) = cfg.paths { - for path in paths { - let is_dir = path.is_dir(); - let dir_files = if is_dir { - // directories will be kept in the config just in case there are new files - directories.push(path.clone()); - path.read_dir() - .map_err(|e| IoError(e, path.clone()))? - .filter_map(Result::ok) - .filter(|f| { - f.path().extension().filter(|e| *e == extension).is_some() - && f.path().is_file() - }) - .map(|f| f.path()) - .collect() - } else if path.is_file() { - vec![path] - } else { - return Err(InvalidFilePath(path.canonicalize().unwrap_or(path))); - }; - for path in dir_files { - let can = path.canonicalize().map_err(|e| IoError(e, path.clone()))?; - if files.contains(&can) { - if !is_dir { - warn!("Ignoring duplicate MBTiles path: {}", can.display()); - } - continue; + for path in cfg.paths { + let is_dir = path.is_dir(); + let dir_files = if is_dir { + // directories will be kept in the config just in case there are new files + directories.push(path.clone()); + path.read_dir() + .map_err(|e| IoError(e, path.clone()))? + .filter_map(Result::ok) + .filter(|f| { + f.path().extension().filter(|e| *e == extension).is_some() && f.path().is_file() + }) + .map(|f| f.path()) + .collect() + } else if path.is_file() { + vec![path] + } else { + return Err(InvalidFilePath(path.canonicalize().unwrap_or(path))); + }; + for path in dir_files { + let can = path.canonicalize().map_err(|e| IoError(e, path.clone()))?; + if files.contains(&can) { + if !is_dir { + warn!("Ignoring duplicate MBTiles path: {}", can.display()); } - let id = path.file_stem().map_or_else( - || "_unknown".to_string(), - |s| s.to_string_lossy().to_string(), - ); - let source = FileConfigSrc::Path(path); - let id = idr.resolve(&id, can.to_string_lossy().to_string()); - info!("Configured source {id} from {}", can.display()); - files.insert(can); - configs.insert(id.clone(), source.clone()); - - let path = match source { - FileConfigSrc::Obj(pmt) => pmt.path, - FileConfigSrc::Path(path) => path, - }; - results.push(create_source(id, path).await?); + continue; } + let id = path.file_stem().map_or_else( + || "_unknown".to_string(), + |s| s.to_string_lossy().to_string(), + ); + let source = FileConfigSrc::Path(path); + let id = idr.resolve(&id, can.to_string_lossy().to_string()); + info!("Configured source {id} from {}", can.display()); + files.insert(can); + configs.insert(id.clone(), source.clone()); + + let path = match source { + FileConfigSrc::Obj(pmt) => pmt.path, + FileConfigSrc::Path(path) => path, + }; + results.push(create_source(id, path).await?); } } @@ -280,7 +283,7 @@ mod tests { let FileConfigEnum::Config(cfg) = cfg else { panic!(); }; - let paths = cfg.paths.clone().unwrap().into_iter().collect::>(); + let paths = cfg.paths.clone().into_iter().collect::>(); assert_eq!( paths, vec![ diff --git a/martin/src/lib.rs b/martin/src/lib.rs index 6915d0517..8903799d0 100644 --- a/martin/src/lib.rs +++ b/martin/src/lib.rs @@ -31,7 +31,7 @@ pub use crate::args::Env; pub use crate::config::{read_config, Config, ServerState}; pub use crate::source::Source; pub use crate::utils::{ - decode_brotli, decode_gzip, BoolOrObject, Error, IdResolver, OneOrMany, Result, + decode_brotli, decode_gzip, Error, IdResolver, OptBoolObj, OptOneMany, Result, }; // Ensure README.md contains valid code diff --git a/martin/src/pg/config.rs b/martin/src/pg/config.rs index 1934756cb..51de0a394 100644 --- a/martin/src/pg/config.rs +++ b/martin/src/pg/config.rs @@ -11,7 +11,7 @@ use crate::pg::config_table::TableInfoSources; use crate::pg::configurator::PgBuilder; use crate::pg::Result; use crate::source::TileInfoSources; -use crate::utils::{on_slow, sorted_opt_map, BoolOrObject, IdResolver, OneOrMany}; +use crate::utils::{on_slow, sorted_opt_map, IdResolver, OptBoolObj, OptOneMany}; pub trait PgInfo { fn format_id(&self) -> String; @@ -47,8 +47,8 @@ pub struct PgConfig { pub max_feature_count: Option, #[serde(skip_serializing_if = "Option::is_none")] pub pool_size: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub auto_publish: Option>, + #[serde(default, skip_serializing_if = "OptBoolObj::is_none")] + pub auto_publish: OptBoolObj, #[serde(skip_serializing_if = "Option::is_none")] #[serde(serialize_with = "sorted_opt_map")] pub tables: Option, @@ -59,29 +59,29 @@ pub struct PgConfig { #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] pub struct PgCfgPublish { - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "OptOneMany::is_none")] #[serde(alias = "from_schema")] - pub from_schemas: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub tables: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub functions: Option>, + pub from_schemas: OptOneMany, + #[serde(default, skip_serializing_if = "OptBoolObj::is_none")] + pub tables: OptBoolObj, + #[serde(default, skip_serializing_if = "OptBoolObj::is_none")] + pub functions: OptBoolObj, } #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -pub struct PgCfgPublishType { - #[serde(skip_serializing_if = "Option::is_none")] +pub struct PgCfgPublishTables { + #[serde(default, skip_serializing_if = "OptOneMany::is_none")] #[serde(alias = "from_schema")] - pub from_schemas: Option>, + pub from_schemas: OptOneMany, #[serde(skip_serializing_if = "Option::is_none")] #[serde(alias = "id_format")] pub source_id_format: Option, /// A table column to use as the feature ID /// If a table has no column with this name, `id_column` will not be set for that table. /// If a list of strings is given, the first found column will be treated as a feature ID. - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default, skip_serializing_if = "OptOneMany::is_none")] #[serde(alias = "id_column")] - pub id_columns: Option>, + pub id_columns: OptOneMany, #[serde(skip_serializing_if = "Option::is_none")] pub clip_geom: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -90,6 +90,16 @@ pub struct PgCfgPublishType { pub extent: Option, } +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +pub struct PgCfgPublishFuncs { + #[serde(default, skip_serializing_if = "OptOneMany::is_none")] + #[serde(alias = "from_schema")] + pub from_schemas: OptOneMany, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(alias = "id_format")] + pub source_id_format: Option, +} + impl PgConfig { /// Apply defaults to the config, and validate if there is a connection string pub fn finalize(&mut self) -> Result { @@ -105,7 +115,7 @@ impl PgConfig { } } if self.tables.is_none() && self.functions.is_none() && self.auto_publish.is_none() { - self.auto_publish = Some(BoolOrObject::Bool(true)); + self.auto_publish = OptBoolObj::Bool(true); } Ok(res) @@ -143,7 +153,7 @@ mod tests { use crate::pg::config_function::FunctionInfo; use crate::pg::config_table::TableInfo; use crate::test_utils::some; - use crate::utils::OneOrMany::{Many, One}; + use crate::utils::OptOneMany::{Many, One}; #[test] fn parse_pg_one() { @@ -153,11 +163,11 @@ mod tests { connection_string: 'postgresql://postgres@localhost/db' "}, &Config { - postgres: Some(One(PgConfig { + postgres: One(PgConfig { connection_string: some("postgresql://postgres@localhost/db"), - auto_publish: Some(BoolOrObject::Bool(true)), + auto_publish: OptBoolObj::Bool(true), ..Default::default() - })), + }), ..Default::default() }, ); @@ -172,18 +182,18 @@ mod tests { - connection_string: 'postgresql://postgres@localhost:5433/db' "}, &Config { - postgres: Some(Many(vec![ + postgres: Many(vec![ PgConfig { connection_string: some("postgres://postgres@localhost:5432/db"), - auto_publish: Some(BoolOrObject::Bool(true)), + auto_publish: OptBoolObj::Bool(true), ..Default::default() }, PgConfig { connection_string: some("postgresql://postgres@localhost:5433/db"), - auto_publish: Some(BoolOrObject::Bool(true)), + auto_publish: OptBoolObj::Bool(true), ..Default::default() }, - ])), + ]), ..Default::default() }, ); @@ -225,7 +235,7 @@ mod tests { bounds: [-180.0, -90.0, 180.0, 90.0] "}, &Config { - postgres: Some(One(PgConfig { + postgres: One(PgConfig { connection_string: some("postgres://postgres@localhost:5432/db"), default_srid: Some(4326), pool_size: Some(20), @@ -262,7 +272,7 @@ mod tests { ), )])), ..Default::default() - })), + }), ..Default::default() }, ); diff --git a/martin/src/pg/configurator.rs b/martin/src/pg/configurator.rs old mode 100755 new mode 100644 index 0cd51d615..7a67a1fc2 --- a/martin/src/pg/configurator.rs +++ b/martin/src/pg/configurator.rs @@ -16,20 +16,42 @@ use crate::pg::table_source::{ }; use crate::pg::utils::{find_info, find_kv_ignore_case, normalize_key, InfoMap}; use crate::pg::PgError::InvalidTableExtent; -use crate::pg::Result; +use crate::pg::{PgCfgPublish, PgCfgPublishFuncs, Result}; use crate::source::TileInfoSources; -use crate::utils::{BoolOrObject, IdResolver, OneOrMany}; +use crate::utils::IdResolver; +use crate::utils::OptOneMany::NoVals; +use crate::OptBoolObj::{Bool, NoValue, Object}; pub type SqlFuncInfoMapMap = InfoMap>; pub type SqlTableInfoMapMapMap = InfoMap>>; #[derive(Debug, PartialEq)] -pub struct PgBuilderAuto { +#[cfg_attr(test, derive(serde::Serialize))] +pub struct PgBuilderFuncs { + #[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))] + schemas: Option>, source_id_format: String, +} + +#[derive(Debug, Default, PartialEq)] +#[cfg_attr(test, derive(serde::Serialize))] +pub struct PgBuilderTables { + #[cfg_attr( + test, + serde( + skip_serializing_if = "Option::is_none", + serialize_with = "crate::utils::sorted_opt_set" + ) + )] schemas: Option>, + source_id_format: String, + #[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))] id_columns: Option>, + #[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))] clip_geom: Option, + #[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))] buffer: Option, + #[cfg_attr(test, serde(skip_serializing_if = "Option::is_none"))] extent: Option, } @@ -39,17 +61,39 @@ pub struct PgBuilder { default_srid: Option, disable_bounds: bool, max_feature_count: Option, - auto_functions: Option, - auto_tables: Option, + auto_functions: Option, + auto_tables: Option, id_resolver: IdResolver, tables: TableInfoSources, functions: FuncInfoSources, } +/// Combine `from_schema` field from the `config.auto_publish` and `config.auto_publish.tables/functions` +macro_rules! get_auto_schemas { + ($config:expr, $typ:ident) => { + if let Object(v) = &$config.auto_publish { + match (&v.from_schemas, &v.$typ) { + (NoVals, NoValue | Bool(_)) => None, + (v, NoValue | Bool(_)) => v.opt_iter().map(|v| v.cloned().collect()), + (NoVals, Object(v)) => v.from_schemas.opt_iter().map(|v| v.cloned().collect()), + (v, Object(v2)) => { + let mut vals: HashSet<_> = v.iter().cloned().collect(); + vals.extend(v2.from_schemas.iter().cloned()); + Some(vals) + } + } + } else { + None + } + }; +} + impl PgBuilder { pub async fn new(config: &PgConfig, id_resolver: IdResolver) -> Result { let pool = PgPool::new(config).await?; + let (auto_tables, auto_functions) = calc_auto(config); + Ok(Self { pool, default_srid: config.default_srid, @@ -58,8 +102,8 @@ impl PgBuilder { id_resolver, tables: config.tables.clone().unwrap_or_default(), functions: config.functions.clone().unwrap_or_default(), - auto_functions: new_auto_publish(config, true), - auto_tables: new_auto_publish(config, false), + auto_functions, + auto_tables, }) } @@ -275,7 +319,7 @@ impl PgBuilder { } } -fn update_auto_fields(id: &str, inf: &mut TableInfo, auto_tables: &PgBuilderAuto) { +fn update_auto_fields(id: &str, inf: &mut TableInfo, auto_tables: &PgBuilderTables) { if inf.clip_geom.is_none() { inf.clip_geom = auto_tables.clip_geom; } @@ -333,83 +377,82 @@ fn update_auto_fields(id: &str, inf: &mut TableInfo, auto_tables: &PgBuilderAuto ); } -fn new_auto_publish(config: &PgConfig, is_function: bool) -> Option { - let default_id_fmt = |is_func| (if is_func { "{function}" } else { "{table}" }).to_string(); - let default = |schemas| { - Some(PgBuilderAuto { - source_id_format: default_id_fmt(is_function), - schemas, - id_columns: None, - clip_geom: None, - buffer: None, - extent: None, - }) +fn calc_auto(config: &PgConfig) -> (Option, Option) { + let auto_tables = if use_auto_publish(config, false) { + let schemas = get_auto_schemas!(config, tables); + let bld = if let Object(PgCfgPublish { + tables: Object(v), .. + }) = &config.auto_publish + { + PgBuilderTables { + schemas, + source_id_format: v + .source_id_format + .as_deref() + .unwrap_or("{table}") + .to_string(), + id_columns: v.id_columns.opt_iter().map(|v| v.cloned().collect()), + clip_geom: v.clip_geom, + buffer: v.buffer, + extent: v.extent, + } + } else { + PgBuilderTables { + schemas, + source_id_format: "{table}".to_string(), + ..Default::default() + } + }; + Some(bld) + } else { + None }; - if let Some(bo_a) = &config.auto_publish { - match bo_a { - BoolOrObject::Object(a) => match if is_function { &a.functions } else { &a.tables } { - Some(bo_i) => match bo_i { - BoolOrObject::Object(item) => Some(PgBuilderAuto { - source_id_format: item - .source_id_format - .as_ref() - .cloned() - .unwrap_or_else(|| default_id_fmt(is_function)), - schemas: merge_opt_hs(&a.from_schemas, &item.from_schemas), - id_columns: item.id_columns.as_ref().and_then(|ids| { - if is_function { - error!("Configuration parameter auto_publish.functions.id_columns is not supported"); - None - } else { - Some(ids.iter().cloned().collect()) - } - }), - clip_geom: { - if is_function { - error!("Configuration parameter auto_publish.functions.clip_geom is not supported"); - None - } else { - item.clip_geom - } - }, - buffer: { - if is_function { - error!("Configuration parameter auto_publish.functions.buffer is not supported"); - None - } else { - item.buffer - } - }, - extent: { - if is_function { - error!("Configuration parameter auto_publish.functions.extent is not supported"); - None - } else { - item.extent - } - }, + let auto_functions = if use_auto_publish(config, true) { + Some(PgBuilderFuncs { + schemas: get_auto_schemas!(config, functions), + source_id_format: if let Object(PgCfgPublish { + functions: + Object(PgCfgPublishFuncs { + source_id_format: Some(v), + .. }), - BoolOrObject::Bool(true) => default(merge_opt_hs(&a.from_schemas, &None)), - BoolOrObject::Bool(false) => None, - }, + .. + }) = &config.auto_publish + { + v.clone() + } else { + "{function}".to_string() + }, + }) + } else { + None + }; + + (auto_tables, auto_functions) +} + +fn use_auto_publish(config: &PgConfig, for_functions: bool) -> bool { + match &config.auto_publish { + NoValue => config.tables.is_none() && config.functions.is_none(), + Object(funcs) => { + if for_functions { // If auto_publish.functions is set, and currently asking for .tables which is missing, // .tables becomes the inverse of functions (i.e. an obj or true in tables means false in functions) - None => match if is_function { &a.tables } else { &a.functions } { - Some(bo_i) => match bo_i { - BoolOrObject::Object(_) | BoolOrObject::Bool(true) => None, - BoolOrObject::Bool(false) => default(merge_opt_hs(&a.from_schemas, &None)), - }, - None => default(merge_opt_hs(&a.from_schemas, &None)), - }, - }, - BoolOrObject::Bool(true) => default(None), - BoolOrObject::Bool(false) => None, + match &funcs.functions { + NoValue => matches!(funcs.tables, NoValue | Bool(false)), + Object(_) => true, + Bool(v) => *v, + } + } else { + match &funcs.tables { + NoValue => matches!(funcs.functions, NoValue | Bool(false)), + Object(_) => true, + Bool(v) => *v, + } + } } - } else if config.tables.is_some() || config.functions.is_some() { - None - } else { - default(None) + Bool(v) => *v, } } @@ -442,142 +485,167 @@ fn by_key(a: &(String, T), b: &(String, T)) -> Ordering { a.0.cmp(&b.0) } -/// Merge two optional list of strings into a hashset -fn merge_opt_hs( - a: &Option>, - b: &Option>, -) -> Option> { - if let Some(a) = a { - let mut res: HashSet<_> = a.iter().cloned().collect(); - if let Some(b) = b { - res.extend(b.iter().cloned()); - } - Some(res) - } else { - b.as_ref().map(|b| b.iter().cloned().collect()) - } -} - #[cfg(test)] mod tests { use indoc::indoc; + use insta::assert_yaml_snapshot; use super::*; - #[allow(clippy::unnecessary_wraps)] - fn builder(source_id_format: &str, schemas: Option<&[&str]>) -> Option { - Some(PgBuilderAuto { - source_id_format: source_id_format.to_string(), - schemas: schemas.map(|s| s.iter().map(|s| (*s).to_string()).collect()), - id_columns: None, - clip_geom: None, - buffer: None, - extent: None, - }) + #[derive(serde::Serialize)] + struct AutoCfg { + auto_table: Option, + auto_funcs: Option, } - - fn parse_yaml(content: &str) -> PgConfig { - serde_yaml::from_str(content).unwrap() + fn auto(content: &str) -> AutoCfg { + let cfg: PgConfig = serde_yaml::from_str(content).unwrap(); + let (auto_table, auto_funcs) = calc_auto(&cfg); + AutoCfg { + auto_table, + auto_funcs, + } } #[test] + #[allow(clippy::too_many_lines)] fn test_auto_publish_no_auto() { - let config = parse_yaml("{}"); - let res = new_auto_publish(&config, false); - assert_eq!(res, builder("{table}", None)); - let res = new_auto_publish(&config, true); - assert_eq!(res, builder("{function}", None)); - - let config = parse_yaml("tables: {}"); - assert_eq!(new_auto_publish(&config, false), None); - assert_eq!(new_auto_publish(&config, true), None); - - let config = parse_yaml("functions: {}"); - assert_eq!(new_auto_publish(&config, false), None); - assert_eq!(new_auto_publish(&config, true), None); - } - - #[test] - fn test_auto_publish_bool() { - let config = parse_yaml("auto_publish: true"); - let res = new_auto_publish(&config, false); - assert_eq!(res, builder("{table}", None)); - let res = new_auto_publish(&config, true); - assert_eq!(res, builder("{function}", None)); - - let config = parse_yaml("auto_publish: false"); - assert_eq!(new_auto_publish(&config, false), None); - assert_eq!(new_auto_publish(&config, true), None); - } - - #[test] - fn test_auto_publish_obj_bool() { - let config = parse_yaml(indoc! {" + let cfg = auto("{}"); + assert_yaml_snapshot!(cfg, @r###" + --- + auto_table: + source_id_format: "{table}" + auto_funcs: + source_id_format: "{function}" + "###); + + let cfg = auto("tables: {}"); + assert_yaml_snapshot!(cfg, @r###" + --- + auto_table: ~ + auto_funcs: ~ + "###); + + let cfg = auto("functions: {}"); + assert_yaml_snapshot!(cfg, @r###" + --- + auto_table: ~ + auto_funcs: ~ + "###); + + let cfg = auto("auto_publish: true"); + assert_yaml_snapshot!(cfg, @r###" + --- + auto_table: + source_id_format: "{table}" + auto_funcs: + source_id_format: "{function}" + "###); + + let cfg = auto("auto_publish: false"); + assert_yaml_snapshot!(cfg, @r###" + --- + auto_table: ~ + auto_funcs: ~ + "###); + + let cfg = auto(indoc! {" auto_publish: from_schemas: public tables: true"}); - let res = new_auto_publish(&config, false); - assert_eq!(res, builder("{table}", Some(&["public"]))); - assert_eq!(new_auto_publish(&config, true), None); - - let config = parse_yaml(indoc! {" + assert_yaml_snapshot!(cfg, @r###" + --- + auto_table: + schemas: + - public + source_id_format: "{table}" + auto_funcs: ~ + "###); + + let cfg = auto(indoc! {" auto_publish: from_schemas: public functions: true"}); - assert_eq!(new_auto_publish(&config, false), None); - let res = new_auto_publish(&config, true); - assert_eq!(res, builder("{function}", Some(&["public"]))); - - let config = parse_yaml(indoc! {" + assert_yaml_snapshot!(cfg, @r###" + --- + auto_table: ~ + auto_funcs: + schemas: + - public + source_id_format: "{function}" + "###); + + let cfg = auto(indoc! {" auto_publish: from_schemas: public tables: false"}); - assert_eq!(new_auto_publish(&config, false), None); - let res = new_auto_publish(&config, true); - assert_eq!(res, builder("{function}", Some(&["public"]))); - - let config = parse_yaml(indoc! {" + assert_yaml_snapshot!(cfg, @r###" + --- + auto_table: ~ + auto_funcs: + schemas: + - public + source_id_format: "{function}" + "###); + + let cfg = auto(indoc! {" auto_publish: from_schemas: public functions: false"}); - let res = new_auto_publish(&config, false); - assert_eq!(res, builder("{table}", Some(&["public"]))); - assert_eq!(new_auto_publish(&config, true), None); - } - - #[test] - fn test_auto_publish_obj_obj() { - let config = parse_yaml(indoc! {" + assert_yaml_snapshot!(cfg, @r###" + --- + auto_table: + schemas: + - public + source_id_format: "{table}" + auto_funcs: ~ + "###); + + let cfg = auto(indoc! {" auto_publish: from_schemas: public tables: from_schemas: osm id_format: 'foo_{schema}.{table}_bar'"}); - let res = new_auto_publish(&config, false); - assert_eq!( - res, - builder("foo_{schema}.{table}_bar", Some(&["public", "osm"])) - ); - assert_eq!(new_auto_publish(&config, true), None); - - let config = parse_yaml(indoc! {" + assert_yaml_snapshot!(cfg, @r###" + --- + auto_table: + schemas: + - osm + - public + source_id_format: "foo_{schema}.{table}_bar" + auto_funcs: ~ + "###); + + let cfg = auto(indoc! {" auto_publish: from_schemas: public tables: from_schemas: osm source_id_format: '{schema}.{table}'"}); - let res = new_auto_publish(&config, false); - assert_eq!(res, builder("{schema}.{table}", Some(&["public", "osm"]))); - assert_eq!(new_auto_publish(&config, true), None); - - let config = parse_yaml(indoc! {" + assert_yaml_snapshot!(cfg, @r###" + --- + auto_table: + schemas: + - osm + - public + source_id_format: "{schema}.{table}" + auto_funcs: ~ + "###); + + let cfg = auto(indoc! {" auto_publish: tables: from_schemas: - osm - public"}); - let res = new_auto_publish(&config, false); - assert_eq!(res, builder("{table}", Some(&["public", "osm"]))); - assert_eq!(new_auto_publish(&config, true), None); + assert_yaml_snapshot!(cfg, @r###" + --- + auto_table: + schemas: + - osm + - public + source_id_format: "{table}" + auto_funcs: ~ + "###); } } diff --git a/martin/src/pg/mod.rs b/martin/src/pg/mod.rs index 3437322e7..35204208b 100644 --- a/martin/src/pg/mod.rs +++ b/martin/src/pg/mod.rs @@ -10,11 +10,9 @@ mod table_source; mod tls; mod utils; -pub use config::{PgCfgPublish, PgCfgPublishType, PgConfig, PgSslCerts}; +pub use config::{PgCfgPublish, PgCfgPublishFuncs, PgCfgPublishTables, PgConfig, PgSslCerts}; pub use config_function::FunctionInfo; pub use config_table::TableInfo; pub use errors::{PgError, Result}; pub use function_source::query_available_function; pub use pool::{PgPool, POOL_SIZE_DEFAULT}; - -pub use crate::utils::BoolOrObject; diff --git a/martin/src/sprites/mod.rs b/martin/src/sprites/mod.rs index cb120d4b9..ac2b3f4a3 100644 --- a/martin/src/sprites/mod.rs +++ b/martin/src/sprites/mod.rs @@ -55,12 +55,11 @@ pub type SpriteCatalog = BTreeMap; pub struct SpriteSources(HashMap); impl SpriteSources { - pub fn resolve(config: &mut Option) -> Result { - let Some(cfg) = config else { + pub fn resolve(config: &mut FileConfigEnum) -> Result { + let Some(cfg) = config.extract_file_config() else { return Ok(Self::default()); }; - let cfg = cfg.extract_file_config(); let mut results = Self::default(); let mut directories = Vec::new(); let mut configs = HashMap::new(); @@ -72,18 +71,16 @@ impl SpriteSources { } }; - if let Some(paths) = cfg.paths { - for path in paths { - let Some(name) = path.file_name() else { - warn!( - "Ignoring sprite source with no name from {}", - path.display() - ); - continue; - }; - directories.push(path.clone()); - results.add_source(name.to_string_lossy().to_string(), path); - } + for path in cfg.paths { + let Some(name) = path.file_name() else { + warn!( + "Ignoring sprite source with no name from {}", + path.display() + ); + continue; + }; + directories.push(path.clone()); + results.add_source(name.to_string_lossy().to_string(), path); } *config = FileConfigEnum::new_extended(directories, configs, cfg.unrecognized); diff --git a/martin/src/utils/cfg_containers.rs b/martin/src/utils/cfg_containers.rs new file mode 100644 index 000000000..6f7c9c2e4 --- /dev/null +++ b/martin/src/utils/cfg_containers.rs @@ -0,0 +1,143 @@ +use std::vec::IntoIter; + +use serde::{Deserialize, Serialize}; + +/// A serde helper to store a boolean as an object. +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum OptBoolObj { + #[default] + #[serde(skip)] + NoValue, + Bool(bool), + Object(T), +} + +impl OptBoolObj { + pub fn is_none(&self) -> bool { + matches!(self, Self::NoValue) + } +} + +#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum OptOneMany { + #[default] + NoVals, + One(T), + Many(Vec), +} + +impl IntoIterator for OptOneMany { + type Item = T; + type IntoIter = IntoIter; + + fn into_iter(self) -> Self::IntoIter { + match self { + Self::NoVals => Vec::new().into_iter(), + Self::One(v) => vec![v].into_iter(), + Self::Many(v) => v.into_iter(), + } + } +} + +impl OptOneMany { + pub fn new>(iter: I) -> Self { + let mut iter = iter.into_iter(); + match (iter.next(), iter.next()) { + (Some(first), Some(second)) => { + let mut vec = Vec::with_capacity(iter.size_hint().0 + 2); + vec.push(first); + vec.push(second); + vec.extend(iter); + Self::Many(vec) + } + (Some(first), None) => Self::One(first), + (None, _) => Self::NoVals, + } + } + + pub fn is_none(&self) -> bool { + matches!(self, Self::NoVals) + } + + pub fn is_empty(&self) -> bool { + match self { + Self::NoVals => true, + Self::One(_) => false, + Self::Many(v) => v.is_empty(), + } + } + + pub fn iter(&self) -> impl Iterator { + match self { + Self::NoVals => [].iter(), + Self::One(v) => std::slice::from_ref(v).iter(), + Self::Many(v) => v.iter(), + } + } + + pub fn opt_iter(&self) -> Option> { + match self { + Self::NoVals => None, + Self::One(v) => Some(std::slice::from_ref(v).iter()), + Self::Many(v) => Some(v.iter()), + } + } + + pub fn iter_mut(&mut self) -> impl Iterator { + match self { + Self::NoVals => [].iter_mut(), + Self::One(v) => std::slice::from_mut(v).iter_mut(), + Self::Many(v) => v.iter_mut(), + } + } + + pub fn as_slice(&self) -> &[T] { + match self { + Self::NoVals => &[], + Self::One(item) => std::slice::from_ref(item), + Self::Many(v) => v.as_slice(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::OptOneMany::{Many, NoVals, One}; + + #[test] + fn test_one_or_many() { + let mut noval: OptOneMany = NoVals; + let mut one = One(1); + let mut many = Many(vec![1, 2, 3]); + + assert_eq!(OptOneMany::new(vec![1, 2, 3]), Many(vec![1, 2, 3])); + assert_eq!(OptOneMany::new(vec![1]), One(1)); + assert_eq!(OptOneMany::new(Vec::::new()), NoVals); + + assert_eq!(noval.iter_mut().collect::>(), Vec::<&i32>::new()); + assert_eq!(one.iter_mut().collect::>(), vec![&1]); + assert_eq!(many.iter_mut().collect::>(), vec![&1, &2, &3]); + + assert_eq!(noval.iter().collect::>(), Vec::<&i32>::new()); + assert_eq!(one.iter().collect::>(), vec![&1]); + assert_eq!(many.iter().collect::>(), vec![&1, &2, &3]); + + assert_eq!(noval.opt_iter().map(Iterator::collect::>), None); + assert_eq!(one.opt_iter().map(Iterator::collect), Some(vec![&1])); + assert_eq!( + many.opt_iter().map(Iterator::collect), + Some(vec![&1, &2, &3]) + ); + + assert_eq!(noval.as_slice(), Vec::::new().as_slice()); + assert_eq!(one.as_slice(), &[1]); + assert_eq!(many.as_slice(), &[1, 2, 3]); + + assert_eq!(noval.into_iter().collect::>(), Vec::::new()); + assert_eq!(one.into_iter().collect::>(), vec![1]); + assert_eq!(many.into_iter().collect::>(), vec![1, 2, 3]); + } +} diff --git a/martin/src/utils/mod.rs b/martin/src/utils/mod.rs index 85534e500..81ab8ec9f 100644 --- a/martin/src/utils/mod.rs +++ b/martin/src/utils/mod.rs @@ -1,11 +1,11 @@ +mod cfg_containers; mod error; mod id_resolver; -mod one_or_many; mod utilities; mod xyz; +pub use cfg_containers::{OptBoolObj, OptOneMany}; pub use error::*; pub use id_resolver::IdResolver; -pub use one_or_many::OneOrMany; pub use utilities::*; pub use xyz::Xyz; diff --git a/martin/src/utils/one_or_many.rs b/martin/src/utils/one_or_many.rs deleted file mode 100644 index c7cdd7808..000000000 --- a/martin/src/utils/one_or_many.rs +++ /dev/null @@ -1,95 +0,0 @@ -use std::vec::IntoIter; - -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(untagged)] -pub enum OneOrMany { - One(T), - Many(Vec), -} - -impl IntoIterator for OneOrMany { - type Item = T; - type IntoIter = IntoIter; - - fn into_iter(self) -> Self::IntoIter { - match self { - Self::One(v) => vec![v].into_iter(), - Self::Many(v) => v.into_iter(), - } - } -} - -impl OneOrMany { - pub fn new_opt>(iter: I) -> Option { - let mut iter = iter.into_iter(); - match (iter.next(), iter.next()) { - (Some(first), Some(second)) => { - let mut vec = Vec::with_capacity(iter.size_hint().0 + 2); - vec.push(first); - vec.push(second); - vec.extend(iter); - Some(Self::Many(vec)) - } - (Some(first), None) => Some(Self::One(first)), - (None, _) => None, - } - } - - pub fn is_empty(&self) -> bool { - match self { - Self::One(_) => false, - Self::Many(v) => v.is_empty(), - } - } - - pub fn iter(&self) -> impl Iterator { - match self { - OneOrMany::Many(v) => v.iter(), - OneOrMany::One(v) => std::slice::from_ref(v).iter(), - } - } - - pub fn iter_mut(&mut self) -> impl Iterator { - match self { - Self::Many(v) => v.iter_mut(), - Self::One(v) => std::slice::from_mut(v).iter_mut(), - } - } - - pub fn as_slice(&self) -> &[T] { - match self { - Self::One(item) => std::slice::from_ref(item), - Self::Many(v) => v.as_slice(), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::OneOrMany::{Many, One}; - - #[test] - fn test_one_or_many() { - let mut one = One(1); - let mut many = Many(vec![1, 2, 3]); - - assert_eq!(OneOrMany::new_opt(vec![1, 2, 3]), Some(Many(vec![1, 2, 3]))); - assert_eq!(OneOrMany::new_opt(vec![1]), Some(One(1))); - assert_eq!(OneOrMany::new_opt(Vec::::new()), None); - - assert_eq!(one.iter_mut().collect::>(), vec![&1]); - assert_eq!(many.iter_mut().collect::>(), vec![&1, &2, &3]); - - assert_eq!(one.iter().collect::>(), vec![&1]); - assert_eq!(many.iter().collect::>(), vec![&1, &2, &3]); - - assert_eq!(one.as_slice(), &[1]); - assert_eq!(many.as_slice(), &[1, 2, 3]); - - assert_eq!(one.into_iter().collect::>(), vec![1]); - assert_eq!(many.into_iter().collect::>(), vec![1, 2, 3]); - } -} diff --git a/martin/src/utils/utilities.rs b/martin/src/utils/utilities.rs index e05e79276..e3a83f352 100644 --- a/martin/src/utils/utilities.rs +++ b/martin/src/utils/utilities.rs @@ -6,17 +6,9 @@ use std::time::Duration; use flate2::read::GzDecoder; use flate2::write::GzEncoder; use futures::pin_mut; -use serde::{Deserialize, Serialize, Serializer}; +use serde::{Serialize, Serializer}; use tokio::time::timeout; -/// A serde helper to store a boolean as an object. -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(untagged)] -pub enum BoolOrObject { - Bool(bool), - Object(T), -} - /// Sort an optional hashmap by key, case-insensitive first, then case-sensitive pub fn sorted_opt_map( value: &Option>, @@ -31,6 +23,21 @@ pub fn sorted_btree_map(value: &HashMap) -> BTreeMa BTreeMap::from_iter(items) } +#[cfg(test)] +pub fn sorted_opt_set( + value: &Option>, + serializer: S, +) -> Result { + value + .as_ref() + .map(|v| { + let mut v: Vec<_> = v.iter().collect(); + v.sort(); + v + }) + .serialize(serializer) +} + pub fn decode_gzip(data: &[u8]) -> Result, std::io::Error> { let mut decoder = GzDecoder::new(data); let mut decompressed = Vec::new(); diff --git a/martin/tests/pg_server_test.rs b/martin/tests/pg_server_test.rs index c814f554d..341e9aabc 100644 --- a/martin/tests/pg_server_test.rs +++ b/martin/tests/pg_server_test.rs @@ -4,7 +4,7 @@ use actix_web::test::{call_and_read_body_json, call_service, read_body, TestRequ use ctor::ctor; use indoc::indoc; use insta::assert_yaml_snapshot; -use martin::OneOrMany; +use martin::OptOneMany; use tilejson::TileJSON; pub mod utils; @@ -1092,7 +1092,7 @@ tables: ) .await; - let OneOrMany::One(cfg) = cfg.postgres.unwrap() else { + let OptOneMany::One(cfg) = cfg.postgres else { panic!() }; for (name, _) in cfg.tables.unwrap_or_default() { diff --git a/martin/tests/utils/pg_utils.rs b/martin/tests/utils/pg_utils.rs index da5da7d3d..27b689420 100644 --- a/martin/tests/utils/pg_utils.rs +++ b/martin/tests/utils/pg_utils.rs @@ -34,8 +34,6 @@ pub fn table<'a>(mock: &'a MockSource, name: &str) -> &'a TableInfo { let (_, config) = mock; let vals: Vec<&TableInfo> = config .postgres - .as_ref() - .unwrap() .iter() .flat_map(|v| v.tables.iter().map(|vv| vv.get(name))) .flatten() From b88d714bf6f540762f0bf9c13f9ac9189884449a Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Sat, 21 Oct 2023 18:41:21 -0400 Subject: [PATCH 069/108] justfile insta install --- justfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/justfile b/justfile index 774a88746..b5f7649b2 100644 --- a/justfile +++ b/justfile @@ -147,12 +147,12 @@ bless: restart clean-test bless-insta-martin bless-insta-mbtiles mv tests/output tests/expected # Run integration tests and save its output as the new expected output -bless-insta-mbtiles *ARGS: (cargo-install "insta" "cargo-insta") +bless-insta-mbtiles *ARGS: (cargo-install "cargo-insta") #rm -rf martin-mbtiles/tests/snapshots cargo insta test --accept --unreferenced=auto -p martin-mbtiles {{ ARGS }} # Run integration tests and save its output as the new expected output -bless-insta-martin *ARGS: (cargo-install "insta" "cargo-insta") +bless-insta-martin *ARGS: (cargo-install "cargo-insta") cargo insta test --accept --unreferenced=auto -p martin {{ ARGS }} # Build and open mdbook documentation From 2053f8067c9d6ae62fcf9f3f1247201eefc213ba Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Sat, 21 Oct 2023 18:54:09 -0400 Subject: [PATCH 070/108] Provide default Source::support_url_query --- martin/src/mbtiles/mod.rs | 4 ---- martin/src/pmtiles/mod.rs | 4 ---- martin/src/source.rs | 4 +++- martin/src/srv/server.rs | 4 ---- 4 files changed, 3 insertions(+), 13 deletions(-) diff --git a/martin/src/mbtiles/mod.rs b/martin/src/mbtiles/mod.rs index efbd74c94..1190ffad1 100644 --- a/martin/src/mbtiles/mod.rs +++ b/martin/src/mbtiles/mod.rs @@ -81,10 +81,6 @@ impl Source for MbtSource { Box::new(self.clone()) } - fn support_url_query(&self) -> bool { - false - } - async fn get_tile(&self, xyz: &Xyz, _url_query: &Option) -> Result { if let Some(tile) = self .mbtiles diff --git a/martin/src/pmtiles/mod.rs b/martin/src/pmtiles/mod.rs index 2af8cf9e5..0b678c928 100644 --- a/martin/src/pmtiles/mod.rs +++ b/martin/src/pmtiles/mod.rs @@ -129,10 +129,6 @@ impl Source for PmtSource { Box::new(self.clone()) } - fn support_url_query(&self) -> bool { - false - } - async fn get_tile(&self, xyz: &Xyz, _url_query: &Option) -> Result { // TODO: optimize to return Bytes if let Some(t) = self diff --git a/martin/src/source.rs b/martin/src/source.rs index bc6767120..f78d69d9d 100644 --- a/martin/src/source.rs +++ b/martin/src/source.rs @@ -105,7 +105,9 @@ pub trait Source: Send + Debug { fn clone_source(&self) -> Box; - fn support_url_query(&self) -> bool; + fn support_url_query(&self) -> bool { + false + } async fn get_tile(&self, xyz: &Xyz, query: &Option) -> Result; diff --git a/martin/src/srv/server.rs b/martin/src/srv/server.rs index 6630f4270..e6c740d85 100755 --- a/martin/src/srv/server.rs +++ b/martin/src/srv/server.rs @@ -505,10 +505,6 @@ mod tests { unimplemented!() } - fn support_url_query(&self) -> bool { - unimplemented!() - } - async fn get_tile( &self, _xyz: &Xyz, From 2e7a179aaf4b7b329fe5b2f0d9e4f10068d59226 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Sat, 21 Oct 2023 21:37:14 -0400 Subject: [PATCH 071/108] Better error reporting for config and other params (#957) This prints an extended error message, explaining to the user what params must be removed. Fixes #938 --- martin/src/args/root.rs | 4 ++-- martin/src/utils/error.rs | 25 +++++++++++++++++++++++-- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/martin/src/args/root.rs b/martin/src/args/root.rs index 79a4846de..844e213c8 100644 --- a/martin/src/args/root.rs +++ b/martin/src/args/root.rs @@ -55,7 +55,7 @@ impl Args { warn!("The WATCH_MODE env variable is no longer supported, and will be ignored"); } if self.meta.config.is_some() && !self.meta.connection.is_empty() { - return Err(Error::ConfigAndConnectionsError); + return Err(Error::ConfigAndConnectionsError(self.meta.connection)); } self.srv.merge_into_config(&mut config.srv); @@ -174,7 +174,7 @@ mod tests { let env = FauxEnv::default(); let mut config = Config::default(); let err = args.merge_into_config(&mut config, &env).unwrap_err(); - assert!(matches!(err, crate::Error::ConfigAndConnectionsError)); + assert!(matches!(err, crate::Error::ConfigAndConnectionsError(..))); } #[test] diff --git a/martin/src/utils/error.rs b/martin/src/utils/error.rs index 44829c34e..a06ddaf6a 100644 --- a/martin/src/utils/error.rs +++ b/martin/src/utils/error.rs @@ -1,3 +1,4 @@ +use std::fmt::Write; use std::io; use std::path::PathBuf; @@ -7,10 +8,30 @@ use crate::sprites::SpriteError; pub type Result = std::result::Result; +fn elide_vec(vec: &[String], max_items: usize, max_len: usize) -> String { + let mut s = String::new(); + for (i, v) in vec.iter().enumerate() { + if i > max_items { + let _ = write!(s, " and {} more", vec.len() - i); + break; + } + if i > 0 { + s.push(' '); + } + if v.len() > max_len { + s.push_str(&v[..max_len]); + s.push('…'); + } else { + s.push_str(v); + } + } + s +} + #[derive(thiserror::Error, Debug)] pub enum Error { - #[error("The --config and the connection parameters cannot be used together")] - ConfigAndConnectionsError, + #[error("The --config and the connection parameters cannot be used together. Please remove unsupported parameters '{}'", elide_vec(.0, 3, 15))] + ConfigAndConnectionsError(Vec), #[error("Unable to bind to {1}: {0}")] BindingError(io::Error, String), From 7aa169ed6734b1284a59fc945821218df8e3065e Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Sun, 22 Oct 2023 03:30:18 -0400 Subject: [PATCH 072/108] [BREAKING] New bounds calculation methods (#958) * Remove `--disable-bounds` flag and `disable_bounds` config parameters. * Add `--auto-bounds` / `-b` CLI parameter and `auto_bounds` config value: * `quick`: Compute table geometry bounds, but abort if it takes longer than 5 seconds (default) * `calc`: Compute table geometry bounds. The startup time may be significant. Make sure all GEO columns have indexes * `skip`: Skip bounds calculation. The bounds will be set to the whole world * `-b` is now mapped to `--auto-bounds` param, but it will fail if used by itself because it now requires a value. Fixes #955 --- Cargo.lock | 2 +- debian/config.yaml | 2 +- docs/src/config-file.md | 8 ++-- docs/src/run-with-cli.md | 26 +++++++++-- martin/Cargo.toml | 2 +- martin/src/args/mod.rs | 3 +- martin/src/args/pg.rs | 29 +++++++++--- martin/src/args/root.rs | 2 +- martin/src/pg/config.rs | 23 ++++++---- martin/src/pg/configurator.rs | 13 +++--- martin/src/pg/table_source.rs | 27 +++++++++-- tests/expected/auto/cmp.json | 6 +++ tests/expected/auto/points3857_srid.json | 6 +++ tests/expected/auto/table_source.json | 6 +++ tests/expected/generated_config.yaml | 57 +++++++++++++++++++++++- tests/test.sh | 2 +- 16 files changed, 176 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 75ed2c211..ca403046d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1693,7 +1693,7 @@ checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "martin" -version = "0.9.3" +version = "0.10.0" dependencies = [ "actix-cors", "actix-http", diff --git a/debian/config.yaml b/debian/config.yaml index aacb3b20a..2f7f99cf1 100644 --- a/debian/config.yaml +++ b/debian/config.yaml @@ -14,7 +14,7 @@ worker_processes: 8 # default_srid: 4326 # pool_size: 20 # max_feature_count: 1000 -# disable_bounds: false +# auto_bounds: skip # pmtiles: # paths: diff --git a/docs/src/config-file.md b/docs/src/config-file.md index 8fe58a638..deaeba64d 100644 --- a/docs/src/config-file.md +++ b/docs/src/config-file.md @@ -47,9 +47,11 @@ postgres: # Limit the number of table geo features included in a tile. Unlimited by default. max_feature_count: 1000 - # Control the automatic generation of bounds for spatial tables [default: false] - # If enabled, it will spend some time on startup to compute geometry bounds. - disable_bounds: false + # Control the automatic generation of bounds for spatial tables [default: quick] + # 'calc' - compute table geometry bounds on startup. + # 'quick' - same as 'calc', but the calculation will be aborted if it takes more than 5 seconds. + # 'skip' - do not compute table geometry bounds on startup. + auto_bounds: skip # Enable automatic discovery of tables and functions. # You may set this to `false` to disable. diff --git a/docs/src/run-with-cli.md b/docs/src/run-with-cli.md index d8c235654..c51d2a139 100644 --- a/docs/src/run-with-cli.md +++ b/docs/src/run-with-cli.md @@ -6,33 +6,51 @@ You can configure Martin using command-line interface. See `martin --help` or `c Usage: martin [OPTIONS] [CONNECTION]... Arguments: - [CONNECTION]... Connection strings, e.g. postgres://... or /path/to/files + [CONNECTION]... + Connection strings, e.g. postgres://... or /path/to/files Options: -c, --config Path to config file. If set, no tile source-related parameters are allowed + --save-config Save resulting config to a file or use "-" to print to stdout. By default, only print if sources are auto-detected + -s, --sprite Export a directory with SVG files as a sprite source. Can be specified multiple times + -k, --keep-alive Connection keep alive timeout. [DEFAULT: 75] + -l, --listen-addresses The socket address to bind. [DEFAULT: 0.0.0.0:3000] + -W, --workers Number of web server workers - -b, --disable-bounds - Disable the automatic generation of bounds for spatial PG tables + + -b, --auto-bounds + Specify how bounds should be computed for the spatial PG tables. [DEFAULT: quick] + + Possible values: + - quick: Compute table geometry bounds, but abort if it takes longer than 5 seconds + - calc: Compute table geometry bounds. The startup time may be significant. Make sure all GEO columns have indexes + - skip: Skip bounds calculation. The bounds will be set to the whole world + --ca-root-file Loads trusted root certificates from a file. The file should contain a sequence of PEM-formatted CA certificates + -d, --default-srid If a spatial PG table has SRID 0, then this default SRID will be used as a fallback + -p, --pool-size Maximum connections pool size [DEFAULT: 20] + -m, --max-feature-count Limit the number of features in a tile from a PG table source + -h, --help - Print help + Print help (see a summary with '-h') + -V, --version Print version ``` diff --git a/martin/Cargo.toml b/martin/Cargo.toml index 38681e00b..800b24139 100644 --- a/martin/Cargo.toml +++ b/martin/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "martin" # Once the release is published with the hash, update https://github.com/maplibre/homebrew-martin -version = "0.9.3" +version = "0.10.0" authors = ["Stepan Kuzmin ", "Yuri Astrakhan ", "MapLibre contributors"] description = "Blazing fast and lightweight tile server with PostGIS, MBTiles, and PMTiles support" keywords = ["maps", "tiles", "mbtiles", "pmtiles", "postgis"] diff --git a/martin/src/args/mod.rs b/martin/src/args/mod.rs index 6866d87ba..daa8a6567 100644 --- a/martin/src/args/mod.rs +++ b/martin/src/args/mod.rs @@ -6,4 +6,5 @@ mod srv; pub use connections::{Arguments, State}; pub use environment::{Env, OsEnv}; -pub use root::Args; +pub use pg::{BoundsCalcType, DEFAULT_BOUNDS_TIMEOUT}; +pub use root::{Args, MetaArgs}; diff --git a/martin/src/args/pg.rs b/martin/src/args/pg.rs index 0ca12136e..2cf18c711 100644 --- a/martin/src/args/pg.rs +++ b/martin/src/args/pg.rs @@ -1,4 +1,8 @@ +use std::time::Duration; + +use clap::ValueEnum; use log::{info, warn}; +use serde::{Deserialize, Serialize}; use crate::args::connections::Arguments; use crate::args::connections::State::{Ignore, Take}; @@ -6,12 +10,27 @@ use crate::args::environment::Env; use crate::pg::{PgConfig, PgSslCerts, POOL_SIZE_DEFAULT}; use crate::utils::{OptBoolObj, OptOneMany}; +// Must match the help string for BoundsType::Quick +pub const DEFAULT_BOUNDS_TIMEOUT: Duration = Duration::from_secs(5); + +#[derive(PartialEq, Eq, Default, Debug, Clone, Copy, Serialize, Deserialize, ValueEnum)] +#[serde(rename_all = "lowercase")] +pub enum BoundsCalcType { + /// Compute table geometry bounds, but abort if it takes longer than 5 seconds. + #[default] + Quick, + /// Compute table geometry bounds. The startup time may be significant. Make sure all GEO columns have indexes. + Calc, + /// Skip bounds calculation. The bounds will be set to the whole world. + Skip, +} + #[derive(clap::Args, Debug, PartialEq, Default)] #[command(about, version)] pub struct PgArgs { - /// Disable the automatic generation of bounds for spatial PG tables. + /// Specify how bounds should be computed for the spatial PG tables. [DEFAULT: quick] #[arg(short = 'b', long)] - pub disable_bounds: bool, + pub auto_bounds: Option, /// Loads trusted root certificates from a file. The file should contain a sequence of PEM-formatted CA certificates. #[arg(long)] pub ca_root_file: Option, @@ -41,11 +60,7 @@ impl PgArgs { connection_string: Some(s), ssl_certificates: certs.clone(), default_srid, - disable_bounds: if self.disable_bounds { - Some(true) - } else { - None - }, + auto_bounds: self.auto_bounds, max_feature_count: self.max_feature_count, pool_size: self.pool_size, auto_publish: OptBoolObj::NoValue, diff --git a/martin/src/args/root.rs b/martin/src/args/root.rs index 844e213c8..98c82560b 100644 --- a/martin/src/args/root.rs +++ b/martin/src/args/root.rs @@ -36,7 +36,7 @@ pub struct MetaArgs { /// By default, only print if sources are auto-detected. #[arg(long)] pub save_config: Option, - /// [Deprecated] Scan for new sources on sources list requests + /// **Deprecated** Scan for new sources on sources list requests #[arg(short, long, hide = true)] pub watch: bool, /// Connection strings, e.g. postgres://... or /path/to/files diff --git a/martin/src/pg/config.rs b/martin/src/pg/config.rs index 51de0a394..5dfa3c1e8 100644 --- a/martin/src/pg/config.rs +++ b/martin/src/pg/config.rs @@ -1,3 +1,4 @@ +use std::ops::Add; use std::time::Duration; use futures::future::try_join; @@ -5,6 +6,7 @@ use log::warn; use serde::{Deserialize, Serialize}; use tilejson::TileJSON; +use crate::args::{BoundsCalcType, DEFAULT_BOUNDS_TIMEOUT}; use crate::config::{copy_unrecognized_config, UnrecognizedValues}; use crate::pg::config_function::FuncInfoSources; use crate::pg::config_table::TableInfoSources; @@ -42,7 +44,7 @@ pub struct PgConfig { #[serde(skip_serializing_if = "Option::is_none")] pub default_srid: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub disable_bounds: Option, + pub auto_bounds: Option, #[serde(skip_serializing_if = "Option::is_none")] pub max_feature_count: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -123,13 +125,18 @@ impl PgConfig { pub async fn resolve(&mut self, id_resolver: IdResolver) -> crate::Result { let pg = PgBuilder::new(self, id_resolver).await?; - let inst_tables = on_slow(pg.instantiate_tables(), Duration::from_secs(5), || { - if pg.disable_bounds() { - warn!("Discovering tables in PostgreSQL database '{}' is taking too long. Bounds calculation is already disabled. You may need to tune your database.", pg.get_id()); - } else { - warn!("Discovering tables in PostgreSQL database '{}' is taking too long. Make sure your table geo columns have a GIS index, or use --disable-bounds CLI/config to skip bbox calculation.", pg.get_id()); - } - }); + let inst_tables = on_slow( + pg.instantiate_tables(), + // warn only if default bounds timeout has already passed + DEFAULT_BOUNDS_TIMEOUT.add(Duration::from_secs(1)), + || { + if pg.auto_bounds() == BoundsCalcType::Skip { + warn!("Discovering tables in PostgreSQL database '{}' is taking too long. Make sure your table geo columns have a GIS index, or use '--auto-bounds skip' CLI/config to skip bbox calculation.", pg.get_id()); + } else { + warn!("Discovering tables in PostgreSQL database '{}' is taking too long. Bounds calculation is already disabled. You may need to tune your database.", pg.get_id()); + } + }, + ); let ((mut tables, tbl_info), (funcs, func_info)) = try_join(inst_tables, pg.instantiate_functions()).await?; diff --git a/martin/src/pg/configurator.rs b/martin/src/pg/configurator.rs index 7a67a1fc2..cefbdba40 100644 --- a/martin/src/pg/configurator.rs +++ b/martin/src/pg/configurator.rs @@ -5,6 +5,7 @@ use futures::future::join_all; use itertools::Itertools; use log::{debug, error, info, warn}; +use crate::args::BoundsCalcType; use crate::pg::config::{PgConfig, PgInfo}; use crate::pg::config_function::{FuncInfoSources, FunctionInfo}; use crate::pg::config_table::{TableInfo, TableInfoSources}; @@ -59,7 +60,7 @@ pub struct PgBuilderTables { pub struct PgBuilder { pool: PgPool, default_srid: Option, - disable_bounds: bool, + auto_bounds: BoundsCalcType, max_feature_count: Option, auto_functions: Option, auto_tables: Option, @@ -97,7 +98,7 @@ impl PgBuilder { Ok(Self { pool, default_srid: config.default_srid, - disable_bounds: config.disable_bounds.unwrap_or_default(), + auto_bounds: config.auto_bounds.unwrap_or_default(), max_feature_count: config.max_feature_count, id_resolver, tables: config.tables.clone().unwrap_or_default(), @@ -107,8 +108,8 @@ impl PgBuilder { }) } - pub fn disable_bounds(&self) -> bool { - self.disable_bounds + pub fn auto_bounds(&self) -> BoundsCalcType { + self.auto_bounds } pub fn get_id(&self) -> &str { @@ -160,7 +161,7 @@ impl PgBuilder { id2, merged_inf, self.pool.clone(), - self.disable_bounds, + self.auto_bounds, self.max_feature_count, )); } @@ -206,7 +207,7 @@ impl PgBuilder { id2, db_inf, self.pool.clone(), - self.disable_bounds, + self.auto_bounds, self.max_feature_count, )); } diff --git a/martin/src/pg/table_source.rs b/martin/src/pg/table_source.rs index d26adad85..5b95b58f7 100644 --- a/martin/src/pg/table_source.rs +++ b/martin/src/pg/table_source.rs @@ -1,11 +1,14 @@ use std::collections::HashMap; +use futures::pin_mut; use log::{debug, info, warn}; use postgis::ewkb; use postgres_protocol::escape::{escape_identifier, escape_literal}; use serde_json::Value; use tilejson::Bounds; +use tokio::time::timeout; +use crate::args::{BoundsCalcType, DEFAULT_BOUNDS_TIMEOUT}; use crate::pg::config::PgInfo; use crate::pg::config_table::TableInfo; use crate::pg::configurator::SqlTableInfoMapMapMap; @@ -96,7 +99,7 @@ pub async fn table_to_query( id: String, mut info: TableInfo, pool: PgPool, - disable_bounds: bool, + bounds_type: BoundsCalcType, max_feature_count: Option, ) -> Result<(String, PgSqlInfo, TableInfo)> { let schema = escape_identifier(&info.schema); @@ -104,8 +107,26 @@ pub async fn table_to_query( let geometry_column = escape_identifier(&info.geometry_column); let srid = info.srid; - if info.bounds.is_none() && !disable_bounds { - info.bounds = calc_bounds(&pool, &schema, &table, &geometry_column, srid).await?; + if info.bounds.is_none() { + match bounds_type { + BoundsCalcType::Skip => {} + BoundsCalcType::Quick | BoundsCalcType::Calc => { + let bounds = calc_bounds(&pool, &schema, &table, &geometry_column, srid); + if bounds_type == BoundsCalcType::Calc { + info.bounds = bounds.await?; + } else { + pin_mut!(bounds); + if let Ok(bounds) = timeout(DEFAULT_BOUNDS_TIMEOUT, &mut bounds).await { + info.bounds = bounds?; + } else { + warn!( + "Timeout computing {} bounds for {id}, aborting query. Use --auto-bounds=calc to wait until complete, or check the table for missing indices.", + info.format_id(), + ); + } + } + } + } } let properties = if let Some(props) = &info.properties { diff --git a/tests/expected/auto/cmp.json b/tests/expected/auto/cmp.json index 234b2bc3d..161878bc1 100644 --- a/tests/expected/auto/cmp.json +++ b/tests/expected/auto/cmp.json @@ -23,6 +23,12 @@ } } ], + "bounds": [ + -179.27313970132585, + -80.46177157848345, + 179.11187181086706, + 84.93092095128937 + ], "description": "public.points1.geom\npublic.points2.geom", "name": "table_source,points1,points2" } diff --git a/tests/expected/auto/points3857_srid.json b/tests/expected/auto/points3857_srid.json index 195123bc5..4b90db678 100644 --- a/tests/expected/auto/points3857_srid.json +++ b/tests/expected/auto/points3857_srid.json @@ -11,6 +11,12 @@ } } ], + "bounds": [ + -161.40590777554058, + -81.50727021609012, + 172.51549126768532, + 84.2440187164111 + ], "description": "public.points3857.geom", "name": "points3857" } diff --git a/tests/expected/auto/table_source.json b/tests/expected/auto/table_source.json index 588e57b37..609c974b5 100644 --- a/tests/expected/auto/table_source.json +++ b/tests/expected/auto/table_source.json @@ -11,6 +11,12 @@ } } ], + "bounds": [ + -2, + -1, + 142.84131509869133, + 45 + ], "name": "table_source", "foo": { "bar": "foo" diff --git a/tests/expected/generated_config.yaml b/tests/expected/generated_config.yaml index e8229fd9a..50bf43f96 100644 --- a/tests/expected/generated_config.yaml +++ b/tests/expected/generated_config.yaml @@ -1,7 +1,7 @@ listen_addresses: localhost:3111 postgres: default_srid: 900913 - disable_bounds: true + auto_bounds: calc auto_publish: true tables: MixPoints: @@ -9,6 +9,11 @@ postgres: table: MixPoints srid: 4326 geometry_column: Geom + bounds: + - -170.94984639004662 + - -84.20025580733805 + - 167.70892858284475 + - 74.23573284753762 geometry_type: POINT properties: Gid: int4 @@ -18,6 +23,11 @@ postgres: table: auto_table srid: 4326 geometry_column: geom + bounds: + - -166.87107126230424 + - -53.44747249115674 + - 168.14061220360549 + - 84.22411861475385 geometry_type: POINT properties: feat_id: int4 @@ -27,6 +37,11 @@ postgres: table: bigint_table srid: 4326 geometry_column: geom + bounds: + - -174.89475564568033 + - -77.2579745396886 + - 174.72753224514435 + - 73.80785950599903 geometry_type: POINT properties: big_feat_id: int8 @@ -36,6 +51,11 @@ postgres: table: points1 srid: 4326 geometry_column: geom + bounds: + - -179.27313970132585 + - -67.52518563265659 + - 162.60117193735186 + - 84.93092095128937 geometry_type: POINT properties: gid: int4 @@ -44,6 +64,11 @@ postgres: table: points1_vw srid: 4326 geometry_column: geom + bounds: + - -179.27313970132585 + - -67.52518563265659 + - 162.60117193735186 + - 84.93092095128937 geometry_type: POINT properties: gid: int4 @@ -52,6 +77,11 @@ postgres: table: points2 srid: 4326 geometry_column: geom + bounds: + - -174.050750735362 + - -80.46177157848345 + - 179.11187181086706 + - 81.13068764165727 geometry_type: POINT properties: gid: int4 @@ -60,6 +90,11 @@ postgres: table: points3857 srid: 3857 geometry_column: geom + bounds: + - -161.40590777554058 + - -81.50727021609012 + - 172.51549126768532 + - 84.2440187164111 geometry_type: POINT properties: gid: int4 @@ -68,6 +103,11 @@ postgres: table: points_empty_srid srid: 900913 geometry_column: geom + bounds: + - -162.35196679784573 + - -84.49919770031491 + - 178.47294677445652 + - 82.7000012450467 geometry_type: GEOMETRY properties: gid: int4 @@ -76,6 +116,11 @@ postgres: table: table_source srid: 4326 geometry_column: geom + bounds: + - -2.0 + - -1.0 + - 142.84131509869133 + - 45.0 geometry_type: GEOMETRY properties: gid: int4 @@ -84,6 +129,11 @@ postgres: table: table_source_multiple_geom srid: 4326 geometry_column: geom1 + bounds: + - -136.62076049706184 + - -78.3350299285405 + - 176.56297743499888 + - 75.78731065954437 geometry_type: POINT properties: gid: int4 @@ -92,6 +142,11 @@ postgres: table: table_source_multiple_geom srid: 4326 geometry_column: geom2 + bounds: + - -136.62076049706184 + - -78.3350299285405 + - 176.56297743499888 + - 75.78731065954437 geometry_type: POINT properties: gid: int4 diff --git a/tests/test.sh b/tests/test.sh index 7cff1b6c0..2ff631790 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -153,7 +153,7 @@ echo "Test auto configured Martin" TEST_OUT_DIR="$(dirname "$0")/output/auto" mkdir -p "$TEST_OUT_DIR" -ARG=(--default-srid 900913 --disable-bounds --save-config "$(dirname "$0")/output/generated_config.yaml" tests/fixtures/mbtiles tests/fixtures/pmtiles) +ARG=(--default-srid 900913 --auto-bounds calc --save-config "$(dirname "$0")/output/generated_config.yaml" tests/fixtures/mbtiles tests/fixtures/pmtiles) set -x $MARTIN_BIN "${ARG[@]}" 2>&1 | tee "${TMP_DIR}/test_log_1.txt" & PROCESS_ID=`jobs -p` From d9eb5fd174c41874725b0856c68f1d3f312e18d2 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Sun, 22 Oct 2023 03:31:01 -0400 Subject: [PATCH 073/108] justfile fixes --- justfile | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/justfile b/justfile index b5f7649b2..ece178f23 100644 --- a/justfile +++ b/justfile @@ -12,7 +12,7 @@ export CARGO_TERM_COLOR := "always" #export RUST_BACKTRACE := "1" @_default: - just --list --unsorted + {{just_executable()}} --list --unsorted # Start Martin server run *ARGS: @@ -25,7 +25,7 @@ run-release *ARGS: start # Start Martin server and open a test page debug-page *ARGS: start open tests/debug.html # run will not exit, so open debug page first - just run {{ ARGS }} + {{just_executable()}} run {{ ARGS }} # Run PSQL utility against the test database psql *ARGS: @@ -71,8 +71,8 @@ alias _stop-db := stop # Restart the test database restart: - just stop - just start + {{just_executable()}} stop + {{just_executable()}} start # Stop the test database stop: @@ -176,8 +176,8 @@ coverage FORMAT='html': (cargo-install "grcov") rustup component add llvm-tools-preview ;\ fi - just clean - just start + {{just_executable()}} clean + {{just_executable()}} start PROF_DIR=target/prof mkdir -p "$PROF_DIR" @@ -251,8 +251,8 @@ clippy: git-pre-push: stop start rustc --version cargo --version - just lint - just test + {{just_executable()}} lint + {{just_executable()}} test # Update sqlite database schema. prepare-sqlite: install-sqlx From 4c97b7c25683d8bccc282fe0b273d7758b108b92 Mon Sep 17 00:00:00 2001 From: D V <77478658+DarhkVoyd@users.noreply.github.com> Date: Tue, 24 Oct 2023 23:16:31 +0530 Subject: [PATCH 074/108] fix: cargo publish warning about README (#960) **Description** - When running `cargo publish -p martin`, the following warning was displayed. ```shell readme `../README.md` appears to be a path outside of the package, but there is already a file named `README.md` in the root of the package. The archived crate will contain the copy in the root of the package. Update the readme to point to the path relative to the root of the package to remove this warning. ``` - Till the time of this PR, the issue was probably a bug with Cargo, in case of a project with a symlink readme linking to the workspace readme. **Changes Made** - To avoid the warning until the issue is fixed with Cargo, the solution is by manually setting `readme = "README.md"` instead of `readme.workspace = true`. **Testing** - [x] I have tried running `cargo publish -p martin` for a few seconds, and hit `Ctrl+C` the moment it starts compiling. The above readme warning is no longer shown. Closes: #914 --- martin/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/martin/Cargo.toml b/martin/Cargo.toml index 800b24139..41dce6fcd 100644 --- a/martin/Cargo.toml +++ b/martin/Cargo.toml @@ -14,7 +14,7 @@ edition.workspace = true license.workspace = true repository.workspace = true rust-version.workspace = true -readme.workspace = true +readme = "README.md" homepage.workspace = true [package.metadata.deb] From 4992267d53c83bcd981e480fa7ead4301a04d008 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Oct 2023 02:52:12 +0000 Subject: [PATCH 075/108] chore(deps): Bump rustls from 0.21.7 to 0.21.8 (#967) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [rustls](https://github.com/rustls/rustls) from 0.21.7 to 0.21.8.
Release notes

Sourced from rustls's releases.

0.21.8

  • Fixes ConnectionCommon::complete_io() to flush writers before potentially expecting a response.
  • Upgrades *ring* to 0.17 - Note: *ring* 0.17 when built with gcc will experience slower X25519 and Ed25519 operations compared to previous releases.
  • Upgrades rustls-webpki to 0.101.7 to match *ring* 0.17 dependency
  • Tls12CipherSuite::hash_algorithm() and Tls13CipherSuite::hash_algorithm() are now crate-internal. This is a small breaking change to remove unintended exposure of underlying *ring* types in the public API.

What's Changed

Full Changelog: https://github.com/rustls/rustls/compare/v/0.21.7...v/0.21.8

Commits
  • c34477a Cargo: 0.21.7 -> 0.21.8
  • 8cf2594 sign: fix clippy get-first warning
  • ecc6cde Flush writers before potentially expecting a response
  • 53adb9d docs: adjust ring platform compatibility
  • d5d6249 upgrade to ring 0.17
  • a659652 tls12/tls13: make hash_algorithm crate internal
  • 3ed39ad upgrade to webpki 0.101.7
  • 3e4a72e Docstrings on expressions are not a thing
  • e26d1d8 Bump MSRV to 1.61
  • ab7b0e7 Drop rust-version metadata for internal crates
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=rustls&package-manager=cargo&previous-version=0.21.7&new-version=0.21.8)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 42 +++++++++++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ca403046d..7873fb650 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2429,11 +2429,25 @@ dependencies = [ "libc", "once_cell", "spin 0.5.2", - "untrusted", + "untrusted 0.7.1", "web-sys", "winapi", ] +[[package]] +name = "ring" +version = "0.17.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb0205304757e5d899b9c2e448b867ffd03ae7f988002e47cd24954391394d0b" +dependencies = [ + "cc", + "getrandom", + "libc", + "spin 0.9.8", + "untrusted 0.9.0", + "windows-sys 0.48.0", +] + [[package]] name = "roxmltree" version = "0.18.1" @@ -2544,12 +2558,12 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.7" +version = "0.21.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" +checksum = "446e14c5cda4f3f30fe71863c34ec70f5ac79d6087097ad0bb433e1be5edf04c" dependencies = [ "log", - "ring", + "ring 0.17.5", "rustls-webpki", "sct", ] @@ -2577,12 +2591,12 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.101.6" +version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ - "ring", - "untrusted", + "ring 0.17.5", + "untrusted 0.9.0", ] [[package]] @@ -2637,8 +2651,8 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" dependencies = [ - "ring", - "untrusted", + "ring 0.16.20", + "untrusted 0.7.1", ] [[package]] @@ -3408,7 +3422,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd5831152cb0d3f79ef5523b357319ba154795d64c7078b2daa95a803b54057f" dependencies = [ "futures", - "ring", + "ring 0.16.20", "rustls", "tokio", "tokio-postgres", @@ -3605,6 +3619,12 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.4.1" From 451139afb4ad62fee16675a12f4316cbf689e3db Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Oct 2023 02:56:19 +0000 Subject: [PATCH 076/108] chore(deps): Bump clap from 4.4.6 to 4.4.7 (#966) Bumps [clap](https://github.com/clap-rs/clap) from 4.4.6 to 4.4.7.
Changelog

Sourced from clap's changelog.

[4.4.7] - 2023-10-24

Performance

  • Reduced code size
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=clap&package-manager=cargo&previous-version=4.4.6&new-version=4.4.7)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7873fb650..946ec5a38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -553,9 +553,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.6" +version = "4.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d04704f56c2cde07f43e8e2c154b43f216dc5c92fc98ada720177362f953b956" +checksum = "ac495e00dcec98c83465d5ad66c5c4fabd652fd6686e7c6269b117e729a6f17b" dependencies = [ "clap_builder", "clap_derive", @@ -563,9 +563,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.6" +version = "4.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e231faeaca65ebd1ea3c737966bf858971cd38c3849107aa3ea7de90a804e45" +checksum = "c77ed9a32a62e6ca27175d00d29d05ca32e396ea1eb5fb01d8256b669cec7663" dependencies = [ "anstream", "anstyle", @@ -575,9 +575,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.4.2" +version = "4.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" +checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" dependencies = [ "heck", "proc-macro2", @@ -587,9 +587,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.5.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" [[package]] name = "color_quant" From 24787d1a3a4b6207bb089f26a95cbb32639d8692 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Tue, 24 Oct 2023 23:16:37 -0400 Subject: [PATCH 077/108] update lock --- Cargo.lock | 45 +++++++++++++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 946ec5a38..4fd1c7ba0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -214,14 +214,15 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "ahash" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" dependencies = [ "cfg-if", "getrandom", "once_cell", "version_check", + "zerocopy", ] [[package]] @@ -396,9 +397,9 @@ dependencies = [ [[package]] name = "base64" -version = "0.21.4" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" [[package]] name = "base64ct" @@ -1834,9 +1835,9 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" dependencies = [ "libc", "log", @@ -1846,9 +1847,9 @@ dependencies = [ [[package]] name = "multimap" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70db9248a93dc36a36d9a47898caa007a32755c7ad140ec64eeeb50d5a730631" +checksum = "e1a5d38b9b352dbd913288736af36af41c48d61b1a8cd34bcecd727561b7d511" dependencies = [ "serde", ] @@ -2647,12 +2648,12 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sct" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ - "ring 0.16.20", - "untrusted 0.7.1", + "ring 0.17.5", + "untrusted 0.9.0", ] [[package]] @@ -4022,6 +4023,26 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" +[[package]] +name = "zerocopy" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69c48d63854f77746c68a5fbb4aa17f3997ece1cb301689a257af8cb80610d21" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c258c1040279e4f88763a113de72ce32dde2d50e2a94573f15dd534cea36a16d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.38", +] + [[package]] name = "zeroize" version = "1.6.0" From 94f2c16267122647bcb8cf054ec8708c94bd6ffd Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Wed, 25 Oct 2023 00:11:50 -0400 Subject: [PATCH 078/108] fix grcov CI workflow (#968) --- .github/workflows/grcov.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/grcov.yml b/.github/workflows/grcov.yml index a79833858..9123bf8c4 100644 --- a/.github/workflows/grcov.yml +++ b/.github/workflows/grcov.yml @@ -54,6 +54,9 @@ jobs: toolchain: nightly override: true + - name: Cleanup GCDA files + run: rm -rf martin/target/debug/deps/*.gcda + - name: Run tests run: cargo test env: @@ -70,9 +73,11 @@ jobs: uses: codecov/codecov-action@v3 with: file: ${{ steps.coverage.outputs.report }} + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - name: Check conditional cfg values run: | - cargo +nightly check -Z unstable-options -Z check-cfg=features,names,values,output --workspace + cargo +nightly check -Z unstable-options -Z check-cfg --workspace env: RUSTFLAGS: '-D warnings' From 06da34c027c760c1fe714ba2ee56d433e0facec0 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Thu, 26 Oct 2023 20:13:00 -0400 Subject: [PATCH 079/108] remove unneeded btree sorting (#970) --- martin/src/source.rs | 2 -- martin/src/sprites/mod.rs | 2 -- martin/src/utils/utilities.rs | 4 +--- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/martin/src/source.rs b/martin/src/source.rs index f78d69d9d..2c0e246e1 100644 --- a/martin/src/source.rs +++ b/martin/src/source.rs @@ -3,7 +3,6 @@ use std::fmt::Debug; use actix_web::error::ErrorNotFound; use async_trait::async_trait; -use itertools::Itertools; use log::debug; use martin_tile_utils::TileInfo; use serde::{Deserialize, Serialize}; @@ -38,7 +37,6 @@ impl TileSources { self.0 .iter() .map(|(id, src)| (id.to_string(), src.get_catalog_entry())) - .sorted_by(|(id1, _), (id2, _)| id1.cmp(id2)) .collect() } diff --git a/martin/src/sprites/mod.rs b/martin/src/sprites/mod.rs index ac2b3f4a3..dc205d6c4 100644 --- a/martin/src/sprites/mod.rs +++ b/martin/src/sprites/mod.rs @@ -4,7 +4,6 @@ use std::fmt::Debug; use std::path::PathBuf; use futures::future::try_join_all; -use itertools::Itertools; use log::{info, warn}; use serde::{Deserialize, Serialize}; use spreet::fs::get_svg_input_paths; @@ -93,7 +92,6 @@ impl SpriteSources { Ok(self .0 .iter() - .sorted_by(|(id1, _), (id2, _)| id1.cmp(id2)) .map(|(id, source)| { let mut images = get_svg_input_paths(&source.path, true) .into_iter() diff --git a/martin/src/utils/utilities.rs b/martin/src/utils/utilities.rs index e3a83f352..a8eab5b21 100644 --- a/martin/src/utils/utilities.rs +++ b/martin/src/utils/utilities.rs @@ -18,9 +18,7 @@ pub fn sorted_opt_map( } pub fn sorted_btree_map(value: &HashMap) -> BTreeMap<&K, &V> { - let mut items: Vec<(_, _)> = value.iter().collect(); - items.sort_by(|a, b| a.0.cmp(b.0)); - BTreeMap::from_iter(items) + value.iter().collect() } #[cfg(test)] From 0ad29492e00ca36f4982e288d0ccd24c437870c0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Oct 2023 02:38:46 +0000 Subject: [PATCH 080/108] chore(deps): Bump serde from 1.0.189 to 1.0.190 (#971) Bumps [serde](https://github.com/serde-rs/serde) from 1.0.189 to 1.0.190.
Release notes

Sourced from serde's releases.

v1.0.190

  • Preserve NaN sign when deserializing f32 from f64 or vice versa (#2637)
Commits
  • edb1a58 Release 1.0.190
  • 11c2917 Merge pull request #2637 from dtolnay/nansign
  • 6ba9c12 Float copysign does not exist in libcore yet
  • d2fcc34 Ensure f32 deserialized from f64 and vice versa preserve NaN sign
  • a091a07 Add float NaN tests
  • bb4135c Fix unused imports
  • 8de84b7 Resolve get_first clippy lint
  • 9cdf332 Remove 'remember to update' reminder from Cargo.toml
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=serde&package-manager=cargo&previous-version=1.0.189&new-version=1.0.190)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4fd1c7ba0..f2cd24fef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2687,18 +2687,18 @@ checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" [[package]] name = "serde" -version = "1.0.189" +version = "1.0.190" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537" +checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.189" +version = "1.0.190" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5" +checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3" dependencies = [ "proc-macro2", "quote", From a53dc12b6df39665936ea94c09396219958a0aa1 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Thu, 26 Oct 2023 23:20:13 -0400 Subject: [PATCH 081/108] update lock --- Cargo.lock | 72 ++++++++++++++++++++++++++---------------------------- 1 file changed, 35 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f2cd24fef..13ab0c6e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -666,9 +666,9 @@ checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" [[package]] name = "cpufeatures" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fbc60abd742b35f2492f808e1abbb83d45f72db402e14c55057edc9c7b1e9e4" +checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" dependencies = [ "libc", ] @@ -1159,9 +1159,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" dependencies = [ "futures-channel", "futures-core", @@ -1174,9 +1174,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" dependencies = [ "futures-core", "futures-sink", @@ -1184,15 +1184,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" [[package]] name = "futures-executor" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" dependencies = [ "futures-core", "futures-task", @@ -1212,15 +1212,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" +checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" [[package]] name = "futures-macro" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" dependencies = [ "proc-macro2", "quote", @@ -1229,15 +1229,15 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" +checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" [[package]] name = "futures-task" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" [[package]] name = "futures-timer" @@ -1247,9 +1247,9 @@ checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" [[package]] name = "futures-util" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" dependencies = [ "futures-channel", "futures-core", @@ -2460,16 +2460,14 @@ dependencies = [ [[package]] name = "rsa" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ab43bb47d23c1a631b4b680199a45255dce26fa9ab2fa902581f624ff13e6a8" +checksum = "86ef35bf3e7fe15a53c4ab08a998e42271eab13eb0db224126bc7bc4c4bad96d" dependencies = [ - "byteorder", "const-oid", "digest", "num-bigint-dig", "num-integer", - "num-iter", "num-traits", "pkcs1", "pkcs8", @@ -2546,9 +2544,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.20" +version = "0.38.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67ce50cb2e16c2903e30d1cbccfd8387a74b9d4c938b6a4c5ec6cc7556f7a8a0" +checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" dependencies = [ "bitflags 2.4.1", "errno", @@ -2752,9 +2750,9 @@ dependencies = [ [[package]] name = "serde_yaml" -version = "0.9.25" +version = "0.9.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a49e178e4452f45cb61d0cd8cebc1b0fafd3e41929e996cef79aa3aca91f574" +checksum = "3cc7a1570e38322cfe4154732e5110f887ea57e22b76f4bfd32b5bdd3368666c" dependencies = [ "indexmap 2.0.2", "itoa", @@ -3229,13 +3227,13 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.8.0" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" +checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" dependencies = [ "cfg-if", "fastrand", - "redox_syscall 0.3.5", + "redox_syscall 0.4.1", "rustix", "windows-sys 0.48.0", ] @@ -3453,9 +3451,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" dependencies = [ "bytes", "futures-core", @@ -4025,18 +4023,18 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" [[package]] name = "zerocopy" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69c48d63854f77746c68a5fbb4aa17f3997ece1cb301689a257af8cb80610d21" +checksum = "81ba595b9f2772fbee2312de30eeb80ec773b4cb2f1e8098db024afadda6c06f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c258c1040279e4f88763a113de72ce32dde2d50e2a94573f15dd534cea36a16d" +checksum = "772666c41fb6dceaf520b564b962d738a8e1a83b41bd48945f50837aed78bb1d" dependencies = [ "proc-macro2", "quote", From 525e14b5fa1d5a91ed080011d0976b0652fb67cb Mon Sep 17 00:00:00 2001 From: Lucas Date: Fri, 27 Oct 2023 20:22:57 +0800 Subject: [PATCH 082/108] Update doc file names and title levels (#972) Fix #962 - [x] Rename doc files for better file organization/order - [x] Update doc links - [x] Adjust title level --- README.md | 10 ++-- debian/config.yaml | 2 +- .../{introduction.md => 00-introduction.md} | 0 .../{installation.md => 10-installation.md} | 10 ++-- docs/src/{run.md => 20-run.md} | 2 +- docs/src/{env-vars.md => 21-env-vars.md} | 4 +- .../{run-with-cli.md => 21-run-with-cli.md} | 2 +- ...n-with-docker.md => 22-run-with-docker.md} | 2 +- ...mpose.md => 23-run-with-docker-compose.md} | 2 +- ...run-with-nginx.md => 24-run-with-nginx.md} | 8 ++-- ...oubleshooting.md => 25-troubleshooting.md} | 2 +- .../src/{config-file.md => 30-config-file.md} | 0 ...pg-connections.md => 31-pg-connections.md} | 6 +-- ...s-pg-tables.md => 32-sources-pg-tables.md} | 10 ++-- ...unctions.md => 33-sources-pg-functions.md} | 10 ++-- .../{sources-files.md => 34-sources-files.md} | 4 +- ...s-composite.md => 35-sources-composite.md} | 2 +- ...urces-sprites.md => 36-sources-sprites.md} | 4 +- docs/src/{using.md => 40-using-endpoints.md} | 14 +++--- ...-maplibre.md => 41-using-with-maplibre.md} | 6 +-- ...th-leaflet.md => 42-using-with-leaflet.md} | 2 +- ...th-deck-gl.md => 43-using-with-deck-gl.md} | 2 +- ...with-mapbox.md => 44-using-with-mapbox.md} | 6 +-- docs/src/{recipes.md => 45-recipes.md} | 6 ++- docs/src/{tools.md => 50-tools.md} | 6 +-- .../src/{development.md => 60-development.md} | 0 docs/src/SUMMARY.md | 48 +++++++++---------- martin-mbtiles/README.md | 4 +- 28 files changed, 88 insertions(+), 86 deletions(-) rename docs/src/{introduction.md => 00-introduction.md} (100%) rename docs/src/{installation.md => 10-installation.md} (94%) rename docs/src/{run.md => 20-run.md} (52%) rename docs/src/{env-vars.md => 21-env-vars.md} (93%) rename docs/src/{run-with-cli.md => 21-run-with-cli.md} (98%) rename docs/src/{run-with-docker.md => 22-run-with-docker.md} (98%) rename docs/src/{run-with-docker-compose.md => 23-run-with-docker-compose.md} (96%) rename docs/src/{run-with-nginx.md => 24-run-with-nginx.md} (96%) rename docs/src/{troubleshooting.md => 25-troubleshooting.md} (97%) rename docs/src/{config-file.md => 30-config-file.md} (100%) rename docs/src/{pg-connections.md => 31-pg-connections.md} (88%) rename docs/src/{sources-pg-tables.md => 32-sources-pg-tables.md} (79%) rename docs/src/{sources-pg-functions.md => 33-sources-pg-functions.md} (97%) rename docs/src/{sources-files.md => 34-sources-files.md} (63%) rename docs/src/{sources-composite.md => 35-sources-composite.md} (97%) rename docs/src/{sources-sprites.md => 36-sources-sprites.md} (95%) rename docs/src/{using.md => 40-using-endpoints.md} (91%) rename docs/src/{using-with-maplibre.md => 41-using-with-maplibre.md} (85%) rename docs/src/{using-with-leaflet.md => 42-using-with-leaflet.md} (96%) rename docs/src/{using-with-deck-gl.md => 43-using-with-deck-gl.md} (98%) rename docs/src/{using-with-mapbox.md => 44-using-with-mapbox.md} (61%) rename docs/src/{recipes.md => 45-recipes.md} (93%) rename docs/src/{tools.md => 50-tools.md} (99%) rename docs/src/{development.md => 60-development.md} (100%) diff --git a/README.md b/README.md index 50dd46fe9..eb7f07b43 100755 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ See [Martin book](https://maplibre.org/martin/) for complete documentation. ![Martin](https://raw.githubusercontent.com/maplibre/martin/main/logo.png) ## Installation -_See [installation instructions](https://maplibre.org/martin/installation.html) in the Martin book._ +_See [installation instructions](https://maplibre.org/martin/10-installation.html) in the Martin book._ **Prerequisites:** If using Martin with PostgreSQL database, you must install PostGIS with at least v3.0+, v3.1+ recommended. @@ -45,11 +45,11 @@ brew install martin ``` ## Running Martin Service -_See [running instructions](https://maplibre.org/martin/run.html) in the Martin book._ +_See [running instructions](https://maplibre.org/martin/20-run.html) in the Martin book._ Martin supports any number of PostgreSQL/PostGIS database connections with [geospatial-enabled](https://postgis.net/docs/using_postgis_dbmanagement.html#geometry_columns) tables and tile-producing SQL functions, as well as [PMTile](https://protomaps.com/blog/pmtiles-v3-whats-new) and [MBTile](https://github.com/mapbox/mbtiles-spec) files as tile sources. -Martin can auto-discover tables and functions using a [connection string](https://maplibre.org/martin/PostgreSQL-Connection-String.html). A PG connection string can also be passed via the `DATABASE_URL` environment variable. +Martin can auto-discover tables and functions using a [connection string](https://maplibre.org/martin/31-pg-connections.html). A PG connection string can also be passed via the `DATABASE_URL` environment variable. Each tile source will have a [TileJSON](https://github.com/mapbox/tilejson-spec) endpoint. @@ -72,7 +72,7 @@ martin --config config.yaml ``` #### Docker Example -_See [Docker instructions](https://maplibre.org/martin/run-with-docker.html) in the Martin book._ +_See [Docker instructions](https://maplibre.org/martin/22-run-with-docker.html) in the Martin book._ Martin is also available as a [Docker image](https://ghcr.io/maplibre/martin). You could either share a configuration file from the host with the container via the `-v` param, or you can let Martin auto-discover all sources e.g. by passing `DATABASE_URL` or specifying the .mbtiles/.pmtiles files. @@ -86,7 +86,7 @@ docker run -p 3000:3000 \ ``` ## API -_See [API documentation](https://maplibre.org/martin/using.html) in the Martin book._ +_See [API documentation](https://maplibre.org/martin/40-using.html) in the Martin book._ Martin data is available via the HTTP `GET` endpoints: diff --git a/debian/config.yaml b/debian/config.yaml index 2f7f99cf1..d9289352a 100644 --- a/debian/config.yaml +++ b/debian/config.yaml @@ -7,7 +7,7 @@ listen_addresses: '0.0.0.0:3000' # Number of web server workers worker_processes: 8 -# see https://maplibre.org/martin/config-file.html +# see https://maplibre.org/martin/30-config-file.html # postgres: # connection_string: 'postgresql://postgres@localhost:5432/db' diff --git a/docs/src/introduction.md b/docs/src/00-introduction.md similarity index 100% rename from docs/src/introduction.md rename to docs/src/00-introduction.md diff --git a/docs/src/installation.md b/docs/src/10-installation.md similarity index 94% rename from docs/src/installation.md rename to docs/src/10-installation.md index 7f0b9a364..114cf3807 100644 --- a/docs/src/installation.md +++ b/docs/src/10-installation.md @@ -1,8 +1,8 @@ -## Prerequisites +### Prerequisites If using Martin with PostgreSQL database, you must install PostGIS with at least v3.0+, v3.1+ recommended. -## Binary Distributions +### Binary Distributions You can download martin from [GitHub releases page](https://github.com/maplibre/martin/releases). @@ -16,7 +16,7 @@ You can download martin from [GitHub releases page](https://github.com/maplibre/ [rl-macos-tar]: https://github.com/maplibre/martin/releases/latest/download/martin-Darwin-x86_64.tar.gz [rl-win64-zip]: https://github.com/maplibre/martin/releases/latest/download/martin-Windows-x86_64.zip -# Building with Cargo +### Building with Cargo If you [install Rust](https://www.rust-lang.org/tools/install), you can build martin from source with Cargo: @@ -25,7 +25,7 @@ cargo install martin martin --help ``` -## Homebrew +### Homebrew If you are using macOS and [Homebrew](https://brew.sh/) you can install martin using Homebrew tap. @@ -34,7 +34,7 @@ brew tap maplibre/martin brew install martin ``` -## Docker +### Docker Martin is also available as a [Docker image](https://ghcr.io/maplibre/martin). You could either share a configuration file from the host with the container via the `-v` param, or you can let Martin auto-discover all sources e.g. by passing `DATABASE_URL` or specifying the .mbtiles/.pmtiles files. diff --git a/docs/src/run.md b/docs/src/20-run.md similarity index 52% rename from docs/src/run.md rename to docs/src/20-run.md index 4007b4cd0..0c1f3c45e 100644 --- a/docs/src/run.md +++ b/docs/src/20-run.md @@ -1,6 +1,6 @@ # Usage -Martin requires at least one PostgreSQL [connection string](pg-connections.md) or a [tile source file](sources-files.md) as a command-line argument. A PG connection string can also be passed via the `DATABASE_URL` environment variable. +Martin requires at least one PostgreSQL [connection string](31-pg-connections.md) or a [tile source file](34-sources-files.md) as a command-line argument. A PG connection string can also be passed via the `DATABASE_URL` environment variable. ```shell martin postgresql://postgres@localhost/db diff --git a/docs/src/env-vars.md b/docs/src/21-env-vars.md similarity index 93% rename from docs/src/env-vars.md rename to docs/src/21-env-vars.md index 54c686439..c55a65209 100644 --- a/docs/src/env-vars.md +++ b/docs/src/21-env-vars.md @@ -1,6 +1,6 @@ -# Environment Variables +## Environment Variables -You can also configure Martin using environment variables, but only if the configuration file is not used. See [configuration section](config-file.md) on how to use environment variables with config files. See also [SSL configuration](pg-connections.md#postgresql-ssl-connections) section below. +You can also configure Martin using environment variables, but only if the configuration file is not used. See [configuration section](30-config-file.md) on how to use environment variables with config files. See also [SSL configuration](31-pg-connections.md#postgresql-ssl-connections) section below. | Environment var
Config File key | Example | Description | |------------------------------------------|--------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| diff --git a/docs/src/run-with-cli.md b/docs/src/21-run-with-cli.md similarity index 98% rename from docs/src/run-with-cli.md rename to docs/src/21-run-with-cli.md index c51d2a139..3354d91d0 100644 --- a/docs/src/run-with-cli.md +++ b/docs/src/21-run-with-cli.md @@ -1,4 +1,4 @@ -# Command-line Interface +## Command-line Interface You can configure Martin using command-line interface. See `martin --help` or `cargo run -- --help` for more information. diff --git a/docs/src/run-with-docker.md b/docs/src/22-run-with-docker.md similarity index 98% rename from docs/src/run-with-docker.md rename to docs/src/22-run-with-docker.md index 08ffa8854..5b60bf445 100644 --- a/docs/src/run-with-docker.md +++ b/docs/src/22-run-with-docker.md @@ -1,4 +1,4 @@ -# Running with Docker +## Running with Docker You can use official Docker image [`ghcr.io/maplibre/martin`](https://ghcr.io/maplibre/martin) diff --git a/docs/src/run-with-docker-compose.md b/docs/src/23-run-with-docker-compose.md similarity index 96% rename from docs/src/run-with-docker-compose.md rename to docs/src/23-run-with-docker-compose.md index 0f4f04ad6..85873eae8 100644 --- a/docs/src/run-with-docker-compose.md +++ b/docs/src/23-run-with-docker-compose.md @@ -1,4 +1,4 @@ -# Running with Docker Compose +## Running with Docker Compose You can use example [`docker-compose.yml`](https://raw.githubusercontent.com/maplibre/martin/main/docker-compose.yml) file as a reference diff --git a/docs/src/run-with-nginx.md b/docs/src/24-run-with-nginx.md similarity index 96% rename from docs/src/run-with-nginx.md rename to docs/src/24-run-with-nginx.md index f3e6a0a02..6230f5e04 100644 --- a/docs/src/run-with-nginx.md +++ b/docs/src/24-run-with-nginx.md @@ -1,4 +1,4 @@ -# Using with NGINX +## Using with NGINX You can run Martin behind NGINX proxy, so you can cache frequently accessed tiles and reduce unnecessary pressure on the database. Here is an example `docker-compose.yml` file that runs Martin with NGINX and PostgreSQL. @@ -38,9 +38,9 @@ services: You can find an example NGINX configuration file [here](https://github.com/maplibre/martin/blob/main/demo/frontend/nginx.conf). -## Rewriting URLs +### Rewriting URLs -If you are running Martin behind NGINX proxy, you may want to rewrite the request URL to properly handle tile URLs in [TileJSON](using.md#source-tilejson). +If you are running Martin behind NGINX proxy, you may want to rewrite the request URL to properly handle tile URLs in [TileJSON](40-using-endpoints.md#source-tilejson). ```nginx location ~ /tiles/(?.*) { @@ -53,7 +53,7 @@ location ~ /tiles/(?.*) { } ``` -## Caching tiles +### Caching tiles You can also use NGINX to cache tiles. In the example, the maximum cache size is set to 10GB, and caching time is set to 1 hour for responses with codes 200, 204, and 302 and 1 minute for responses with code 404. diff --git a/docs/src/troubleshooting.md b/docs/src/25-troubleshooting.md similarity index 97% rename from docs/src/troubleshooting.md rename to docs/src/25-troubleshooting.md index 489c37f6c..0c46f4478 100644 --- a/docs/src/troubleshooting.md +++ b/docs/src/25-troubleshooting.md @@ -1,4 +1,4 @@ -# Troubleshooting +## Troubleshooting Log levels are controlled on a per-module basis, and by default all logging is disabled except for errors. Logging is controlled via the `RUST_LOG` environment variable. The value of this environment variable is a comma-separated list of logging directives. diff --git a/docs/src/config-file.md b/docs/src/30-config-file.md similarity index 100% rename from docs/src/config-file.md rename to docs/src/30-config-file.md diff --git a/docs/src/pg-connections.md b/docs/src/31-pg-connections.md similarity index 88% rename from docs/src/pg-connections.md rename to docs/src/31-pg-connections.md index d887e4cfd..605cd560e 100644 --- a/docs/src/pg-connections.md +++ b/docs/src/31-pg-connections.md @@ -1,9 +1,9 @@ -# PostgreSQL Connection String +## PostgreSQL Connection String Martin supports many of the PostgreSQL connection string settings such as `host`, `port`, `user`, `password`, `dbname`, `sslmode`, `connect_timeout`, `keepalives`, `keepalives_idle`, etc. See the [PostgreSQL docs](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING) for more details. -## PostgreSQL SSL Connections +### PostgreSQL SSL Connections -Martin supports PostgreSQL `sslmode` including `disable`, `prefer`, `require`, `verify-ca` and `verify-full` modes as described in the [PostgreSQL docs](https://www.postgresql.org/docs/current/libpq-ssl.html). Certificates can be provided in the configuration file, or can be set using the same env vars as used for `psql`. When set as env vars, they apply to all PostgreSQL connections. See [environment vars](env-vars.md) section for more details. +Martin supports PostgreSQL `sslmode` including `disable`, `prefer`, `require`, `verify-ca` and `verify-full` modes as described in the [PostgreSQL docs](https://www.postgresql.org/docs/current/libpq-ssl.html). Certificates can be provided in the configuration file, or can be set using the same env vars as used for `psql`. When set as env vars, they apply to all PostgreSQL connections. See [environment vars](21-env-vars.md) section for more details. By default, `sslmode` is set to `prefer` which means that SSL is used if the server supports it, but the connection is not aborted if the server does not support it. This is the default behavior of `psql` and is the most compatible option. Use the `sslmode` param to set a different `sslmode`, e.g. `postgresql://user:password@host/db?sslmode=require`. diff --git a/docs/src/sources-pg-tables.md b/docs/src/32-sources-pg-tables.md similarity index 79% rename from docs/src/sources-pg-tables.md rename to docs/src/32-sources-pg-tables.md index 928b49f0e..6514acc1d 100644 --- a/docs/src/sources-pg-tables.md +++ b/docs/src/32-sources-pg-tables.md @@ -1,8 +1,8 @@ -# Table Sources +## Table Sources -Table Source is a database table which can be used to query [vector tiles](https://github.com/mapbox/vector-tile-spec). If a [PostgreSQL connection string](pg-connections.md) is given, Martin will publish all tables as data sources if they have at least one geometry column. If geometry column SRID is 0, a default SRID must be set, or else that geo-column/table will be ignored. All non-geometry table columns will be published as vector tile feature tags (properties). +Table Source is a database table which can be used to query [vector tiles](https://github.com/mapbox/vector-tile-spec). If a [PostgreSQL connection string](31-pg-connections.md) is given, Martin will publish all tables as data sources if they have at least one geometry column. If geometry column SRID is 0, a default SRID must be set, or else that geo-column/table will be ignored. All non-geometry table columns will be published as vector tile feature tags (properties). -# Modifying Tilejson +### Modifying Tilejson Martin will automatically generate a `TileJSON` manifest for each table source. It will contain the `name`, `description`, `minzoom`, `maxzoom`, `bounds` and `vector_layer` information. For example, if there is a table `public.table_source`: @@ -39,9 +39,9 @@ The TileJSON: } ``` -By default the `description` and `name` is database identifies about this table, and the bounds is queried from database. You can fine tune these by adjusting `auto_publish` section in [configuration file](https://maplibre.org/martin/config-file.html#config-example). +By default the `description` and `name` is database identifies about this table, and the bounds is queried from database. You can fine tune these by adjusting `auto_publish` section in [configuration file](https://maplibre.org/martin/30-config-file.html#config-example). -## TileJSON in SQL Comments +#### TileJSON in SQL Comments Other than adjusting `auto_publish` section in configuration file, you can fine tune the `TileJSON` on the database side directly: Add a valid JSON as an SQL comment on the table. diff --git a/docs/src/sources-pg-functions.md b/docs/src/33-sources-pg-functions.md similarity index 97% rename from docs/src/sources-pg-functions.md rename to docs/src/33-sources-pg-functions.md index 7f8906c9c..b18c83862 100644 --- a/docs/src/sources-pg-functions.md +++ b/docs/src/33-sources-pg-functions.md @@ -1,4 +1,4 @@ -# PostgreSQL Function Sources +## PostgreSQL Function Sources Function Source is a database function which can be used to query [vector tiles](https://github.com/mapbox/vector-tile-spec). When started, Martin will look for the functions with a suitable signature. A function that takes `z integer` (or `zoom integer`), `x integer`, `y integer`, and an optional `query json` and returns `bytea`, can be used as a Function Source. Alternatively the function could return a record with a single `bytea` field, or a record with two fields of types `bytea` and `text`, where the `text` field is an etag key (i.e. md5 hash). @@ -9,7 +9,7 @@ Function Source is a database function which can be used to query [vector tiles] | y | integer | Tile y parameter | | query (optional, any name) | json | Query string parameters | -## Simple Function +### Simple Function For example, if you have a table `table_source` in WGS84 (`4326` SRID), then you can use this function as a Function Source: ```sql, ignore @@ -34,7 +34,7 @@ END $$ LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE; ``` -## Function with Query Parameters +### Function with Query Parameters Users may add a `query` parameter to pass additional parameters to the function. _**TODO**: Modify this example to actually use the query parameters._ @@ -97,7 +97,7 @@ You can access this params using [json operators](https://www.postgresql.org/doc ...WHERE answer = (query_params->'objectParam'->>'answer')::int; ``` -## Modifying TileJSON +### Modifying TileJSON Martin will automatically generate a basic [TileJSON](https://github.com/mapbox/tilejson-spec) manifest for each function source that will contain the name and description of the function, plus optionally `minzoom`, `maxzoom`, and `bounds` (if they were specified via one of the configuration methods). For example, if there is a function `public.function_zxy_query_jsonb`, the default `TileJSON` might look like this (note that URL will be automatically adjusted to match the request host): @@ -112,7 +112,7 @@ Martin will automatically generate a basic [TileJSON](https://github.com/mapbox/ } ``` -### TileJSON in SQL Comments +#### TileJSON in SQL Comments To modify automatically generated `TileJSON`, you can add a valid JSON as an SQL comment on the function. Martin will merge function comment into the generated `TileJSON` using [JSON Merge patch](https://www.rfc-editor.org/rfc/rfc7386). The following example adds `attribution` and `version` fields to the `TileJSON`. diff --git a/docs/src/sources-files.md b/docs/src/34-sources-files.md similarity index 63% rename from docs/src/sources-files.md rename to docs/src/34-sources-files.md index 8f80ba46b..85476f5bc 100644 --- a/docs/src/sources-files.md +++ b/docs/src/34-sources-files.md @@ -1,4 +1,4 @@ -# MBTiles and PMTiles File Sources +## MBTiles and PMTiles File Sources Martin can serve any type of tiles from [PMTile](https://protomaps.com/blog/pmtiles-v3-whats-new) and [MBTile](https://github.com/mapbox/mbtiles-spec) files. To serve a file from CLI, simply put the path to the file or the directory with `*.mbtiles` or `*.pmtiles` files. For example: @@ -6,4 +6,4 @@ Martin can serve any type of tiles from [PMTile](https://protomaps.com/blog/pmti martin /path/to/mbtiles/file.mbtiles /path/to/directory ``` -You may also want to generate a [config file](config-file.md) using the `--save-config my-config.yaml`, and later edit it and use it with `--config my-config.yaml` option. +You may also want to generate a [config file](30-config-file.md) using the `--save-config my-config.yaml`, and later edit it and use it with `--config my-config.yaml` option. diff --git a/docs/src/sources-composite.md b/docs/src/35-sources-composite.md similarity index 97% rename from docs/src/sources-composite.md rename to docs/src/35-sources-composite.md index e580ab9f3..255b5caf0 100644 --- a/docs/src/sources-composite.md +++ b/docs/src/35-sources-composite.md @@ -1,4 +1,4 @@ -# Composite Sources +## Composite Sources Composite Sources allows combining multiple sources into one. Composite Source consists of multiple sources separated by comma `{source1},...,{sourceN}` diff --git a/docs/src/sources-sprites.md b/docs/src/36-sources-sprites.md similarity index 95% rename from docs/src/sources-sprites.md rename to docs/src/36-sources-sprites.md index 9925360c0..76b742028 100644 --- a/docs/src/sources-sprites.md +++ b/docs/src/36-sources-sprites.md @@ -1,4 +1,4 @@ -# Sprite Sources +## Sprite Sources Given a directory with SVG images, Martin will generate a sprite -- a JSON index and a PNG image, for both low and high resolution displays. The SVG filenames without extension will be used as the sprite image IDs. The images are searched recursively in the given directory, so subdirectory names will be used as prefixes for the image IDs, e.g. `icons/bicycle.svg` will be available as `icons/bicycle` sprite image. @@ -39,7 +39,7 @@ martin --sprite /path/to/sprite_a --sprite /path/to/other/sprite_b ### Configuring with Config File -A sprite directory can be configured from the config file with the `sprite` key, similar to how [MBTiles and PMTiles](config-file.md) are configured. +A sprite directory can be configured from the config file with the `sprite` key, similar to how [MBTiles and PMTiles](30-config-file.md) are configured. ```yaml # Sprite configuration diff --git a/docs/src/using.md b/docs/src/40-using-endpoints.md similarity index 91% rename from docs/src/using.md rename to docs/src/40-using-endpoints.md index a42fbd26e..5130845aa 100644 --- a/docs/src/using.md +++ b/docs/src/40-using-endpoints.md @@ -1,4 +1,4 @@ -# Martin Endpoints +## Martin Endpoints Martin data is available via the HTTP `GET` endpoints: @@ -9,20 +9,20 @@ Martin data is available via the HTTP `GET` endpoints: | `/{sourceID}` | [Source TileJSON](#source-tilejson) | | `/{sourceID}/{z}/{x}/{y}` | Map Tiles | | `/{source1},...,{sourceN}` | [Composite Source TileJSON](#source-tilejson) | -| `/{source1},...,{sourceN}/{z}/{x}/{y}` | [Composite Source Tiles](sources-composite.md) | -| `/sprite/{spriteID}[@2x].{json,png}` | [Sprite sources](sources-sprites.md) | +| `/{source1},...,{sourceN}/{z}/{x}/{y}` | [Composite Source Tiles](30-config-file.md) | +| `/sprite/{spriteID}[@2x].{json,png}` | [Sprite sources](36-sources-spritess.md) | | `/health` | Martin server health check: returns 200 `OK` | -## Duplicate Source ID +### Duplicate Source ID In case there is more than one source that has the same name, e.g. a PG function is available in two schemas/connections, or a table has more than one geometry columns, sources will be assigned unique IDs such as `/points`, `/points.1`, etc. -## Reserved Source IDs +### Reserved Source IDs Some source IDs are reserved for internal use. If you try to use them, they will be automatically renamed to a unique ID the same way as duplicate source IDs are handled, e.g. a `catalog` source will become `catalog.1`. Some of the reserved IDs: `_`, `catalog`, `config`, `font`, `health`, `help`, `index`, `manifest`, `metrics`, `refresh`, `reload`, `sprite`, `status`. -## Catalog +### Catalog A list of all available sources is available via catalogue endpoint: @@ -46,7 +46,7 @@ curl localhost:3000/catalog | jq } ``` -## Source TileJSON +### Source TileJSON All tile sources have a [TileJSON](https://github.com/mapbox/tilejson-spec) endpoint available at the `/{SourceID}`. diff --git a/docs/src/using-with-maplibre.md b/docs/src/41-using-with-maplibre.md similarity index 85% rename from docs/src/using-with-maplibre.md rename to docs/src/41-using-with-maplibre.md index 3b63e81d1..edfd22195 100644 --- a/docs/src/using-with-maplibre.md +++ b/docs/src/41-using-with-maplibre.md @@ -1,8 +1,8 @@ -# Using with MapLibre +## Using with MapLibre [MapLibre](https://maplibre.org/projects/maplibre-gl-js/) is an Open-source JavaScript library for showing maps on a website. MapLibre can accept [MVT vector tiles](https://github.com/mapbox/vector-tile-spec) generated by Martin, and applies [a style](https://maplibre.org/maplibre-gl-js-docs/style-spec/) to them to draw a map using Web GL. -You can add a layer to the map and specify Martin [TileJSON](https://github.com/mapbox/tilejson-spec) endpoint as a vector source URL. You should also specify a `source-layer` property. For [Table Sources](sources-pg-tables.md) it is `{table_name}` by default. +You can add a layer to the map and specify Martin [TileJSON](https://github.com/mapbox/tilejson-spec) endpoint as a vector source URL. You should also specify a `source-layer` property. For [Table Sources](32-sources-pg-tables.md) it is `{table_name}` by default. ```js @@ -36,7 +36,7 @@ map.addLayer({ }); ``` -You can also combine multiple sources into one source with [Composite Sources](sources-composite.md). Each source in a composite source can be accessed with its `{source_name}` as a `source-layer` property. +You can also combine multiple sources into one source with [Composite Sources](35-sources-composite.md). Each source in a composite source can be accessed with its `{source_name}` as a `source-layer` property. ```js map.addSource('points', { diff --git a/docs/src/using-with-leaflet.md b/docs/src/42-using-with-leaflet.md similarity index 96% rename from docs/src/using-with-leaflet.md rename to docs/src/42-using-with-leaflet.md index 842a18039..a0eaeba21 100644 --- a/docs/src/using-with-leaflet.md +++ b/docs/src/42-using-with-leaflet.md @@ -1,4 +1,4 @@ -# Using with Leaflet +## Using with Leaflet [Leaflet](https://github.com/Leaflet/Leaflet) is the leading open-source JavaScript library for mobile-friendly interactive maps. diff --git a/docs/src/using-with-deck-gl.md b/docs/src/43-using-with-deck-gl.md similarity index 98% rename from docs/src/using-with-deck-gl.md rename to docs/src/43-using-with-deck-gl.md index 723cd9108..b459564f0 100644 --- a/docs/src/using-with-deck-gl.md +++ b/docs/src/43-using-with-deck-gl.md @@ -1,4 +1,4 @@ -# Using with deck.gl +## Using with deck.gl [deck.gl](https://deck.gl/) is a WebGL-powered framework for visual exploratory data analysis of large datasets. diff --git a/docs/src/using-with-mapbox.md b/docs/src/44-using-with-mapbox.md similarity index 61% rename from docs/src/using-with-mapbox.md rename to docs/src/44-using-with-mapbox.md index 9203d885a..e0d1ac0ca 100644 --- a/docs/src/using-with-mapbox.md +++ b/docs/src/44-using-with-mapbox.md @@ -1,8 +1,8 @@ -# Using with Mapbox +## Using with Mapbox -[Mapbox GL JS](https://github.com/mapbox/mapbox-gl-js) is a JavaScript library for interactive, customizable vector maps on the web. Mapbox GL JS v1.x was open source, and it was forked as MapLibre, so using Martin with Mapbox is similar to MapLibre described [here](using-with-maplibre.md). Mapbox GL JS can accept [MVT vector tiles](https://github.com/mapbox/vector-tile-spec) generated by Martin, and applies [a style](https://docs.mapbox.com/mapbox-gl-js/style-spec/) to them to draw a map using Web GL. +[Mapbox GL JS](https://github.com/mapbox/mapbox-gl-js) is a JavaScript library for interactive, customizable vector maps on the web. Mapbox GL JS v1.x was open source, and it was forked as MapLibre, so using Martin with Mapbox is similar to MapLibre described [here](41-using-with-maplibre.md). Mapbox GL JS can accept [MVT vector tiles](https://github.com/mapbox/vector-tile-spec) generated by Martin, and applies [a style](https://docs.mapbox.com/mapbox-gl-js/style-spec/) to them to draw a map using Web GL. -You can add a layer to the map and specify Martin TileJSON endpoint as a vector source URL. You should also specify a `source-layer` property. For [Table Sources](sources-pg-tables.md) it is `{table_name}` by default. +You can add a layer to the map and specify Martin TileJSON endpoint as a vector source URL. You should also specify a `source-layer` property. For [Table Sources](32-sources-pg-tables.md) it is `{table_name}` by default. ```js map.addLayer({ diff --git a/docs/src/recipes.md b/docs/src/45-recipes.md similarity index 93% rename from docs/src/recipes.md rename to docs/src/45-recipes.md index e2a3e6de2..fb2dfe6d9 100644 --- a/docs/src/recipes.md +++ b/docs/src/45-recipes.md @@ -1,4 +1,6 @@ -## Using with DigitalOcean PostgreSQL +## Recipes + +### Using with DigitalOcean PostgreSQL You can use Martin with [Managed PostgreSQL from DigitalOcean](https://www.digitalocean.com/products/managed-databases-postgresql/) with PostGIS extension @@ -9,7 +11,7 @@ martin --ca-root-file ./ca-certificate.crt \ postgresql://user:password@host:port/db?sslmode=require ``` -## Using with Heroku PostgreSQL +### Using with Heroku PostgreSQL You can use Martin with [Managed PostgreSQL from Heroku](https://www.heroku.com/postgres) with PostGIS extension diff --git a/docs/src/tools.md b/docs/src/50-tools.md similarity index 99% rename from docs/src/tools.md rename to docs/src/50-tools.md index d0fded3c1..6d5c0dab6 100644 --- a/docs/src/tools.md +++ b/docs/src/50-tools.md @@ -89,13 +89,13 @@ The `mbtiles` tool will compute `agg_tiles_hash` value when copying or validatin ## Supported Schema The `mbtiles` tool supports three different kinds of schema for `tiles` data in `.mbtiles` files. See also the original [specification](https://github.com/mapbox/mbtiles-spec#readme). -#### flat +### flat ```sql, ignore CREATE TABLE tiles (zoom_level integer, tile_column integer, tile_row integer, tile_data blob); CREATE UNIQUE INDEX tile_index on tiles (zoom_level, tile_column, tile_row); ``` -#### flat-with-hash +### flat-with-hash ```sql, ignore CREATE TABLE tiles_with_hash ( zoom_level integer NOT NULL, @@ -107,7 +107,7 @@ CREATE UNIQUE INDEX tiles_with_hash_index on tiles_with_hash (zoom_level, tile_c CREATE VIEW tiles AS SELECT zoom_level, tile_column, tile_row, tile_data FROM tiles_with_hash; ``` -#### normalized +### normalized ```sql, ignore CREATE TABLE map (zoom_level INTEGER, tile_column INTEGER, tile_row INTEGER, tile_id TEXT); CREATE UNIQUE INDEX map_index ON map (zoom_level, tile_column, tile_row); diff --git a/docs/src/development.md b/docs/src/60-development.md similarity index 100% rename from docs/src/development.md rename to docs/src/60-development.md diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index c306389b3..b63349be0 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -1,24 +1,24 @@ -[Introduction](introduction.md) -- [Installation](installation.md) -- [Running](run.md) - - [Command Line Interface](run-with-cli.md) - - [Environment Variables](env-vars.md) - - [Running with Docker](run-with-docker.md) - - [Running with Docker Compose](run-with-docker-compose.md) - - [Running with NGINX](run-with-nginx.md) - - [Troubleshooting](troubleshooting.md) -- [Configuration File](config-file.md) - - [PostgreSQL Connections](pg-connections.md) - - [PostgreSQL Table Sources](sources-pg-tables.md) - - [PostgreSQL Function Sources](sources-pg-functions.md) - - [MBTiles and PMTiles File Sources](sources-files.md) - - [Composite Sources](sources-composite.md) - - [Sprite Sources](sources-sprites.md) -- [Usage and Endpoint API](using.md) - - [Using with MapLibre](using-with-maplibre.md) - - [Using with Leaflet](using-with-leaflet.md) - - [Using with deck.gl](using-with-deck-gl.md) - - [Using with Mapbox](using-with-mapbox.md) - - [Recipes](recipes.md) -- [Tools](tools.md) -- [Development](development.md) +[Introduction](00-introduction.md) +- [Installation](10-installation.md) +- [Running](20-run.md) + - [Command Line Interface](21-run-with-cli.md) + - [Environment Variables](21-env-vars.md) + - [Running with Docker](22-run-with-docker.md) + - [Running with Docker Compose](23-run-with-docker-compose.md) + - [Running with NGINX](24-run-with-nginx.md) + - [Troubleshooting](25-troubleshooting.md) +- [Configuration File](30-config-file.md) + - [PostgreSQL Connections](31-pg-connections.md) + - [PostgreSQL Table Sources](32-sources-pg-tables.md) + - [PostgreSQL Function Sources](33-sources-pg-functions.md) + - [MBTiles and PMTiles File Sources](34-sources-files.md) + - [Composite Sources](35-sources-composite.md) + - [Sprite Sources](36-sources-sprites.md) +- [Usage and Endpoint API](40-using-endpoints.md) + - [Using with MapLibre](41-using-with-maplibre.md) + - [Using with Leaflet](42-using-with-leaflet.md) + - [Using with deck.gl](43-using-with-deck-gl.md) + - [Using with Mapbox](44-using-with-mapbox.md) + - [Recipes](45-recipes.md) +- [Tools](50-tools.md) +- [Development](60-development.md) diff --git a/martin-mbtiles/README.md b/martin-mbtiles/README.md index 04f8f5a7b..2963f4076 100644 --- a/martin-mbtiles/README.md +++ b/martin-mbtiles/README.md @@ -1,6 +1,6 @@ # martin-mbtiles -[![Book](https://img.shields.io/badge/docs-Book-informational)](https://maplibre.org/martin/tools.html) +[![Book](https://img.shields.io/badge/docs-Book-informational)](https://maplibre.org/martin/50-tools.html) [![docs.rs docs](https://docs.rs/martin-mbtiles/badge.svg)](https://docs.rs/martin-mbtiles) [![Slack chat](https://img.shields.io/badge/Chat-on%20Slack-blueviolet)](https://slack.openstreetmap.us/) [![GitHub](https://img.shields.io/badge/github-maplibre/martin-8da0cb?logo=github)](https://github.com/maplibre/martin) @@ -9,7 +9,7 @@ A library to help tile servers like [Martin](https://maplibre.org/martin) work with [MBTiles](https://github.com/mapbox/mbtiles-spec) files. When using as a lib, you may want to disable default features (i.e. the unused "cli" feature). -This crate also has a small utility that allows users to interact with the `*.mbtiles` files from the command line. See [tools](https://maplibre.org/martin/tools.html) documentation for more information. +This crate also has a small utility that allows users to interact with the `*.mbtiles` files from the command line. See [tools](https://maplibre.org/martin/50-tools.html) documentation for more information. ### Development From dbf25f041c323ddef8ff4e2534df0fc9a598fc9a Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Fri, 27 Oct 2023 22:26:23 -0400 Subject: [PATCH 083/108] update lock --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 13ab0c6e5..f868394d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4023,18 +4023,18 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" [[package]] name = "zerocopy" -version = "0.7.15" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81ba595b9f2772fbee2312de30eeb80ec773b4cb2f1e8098db024afadda6c06f" +checksum = "ede7d7c7970ca2215b8c1ccf4d4f354c4733201dfaaba72d44ae5b37472e4901" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.15" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "772666c41fb6dceaf520b564b962d738a8e1a83b41bd48945f50837aed78bb1d" +checksum = "4b27b1bb92570f989aac0ab7e9cbfbacdd65973f7ee920d9f0e71ebac878fd0b" dependencies = [ "proc-macro2", "quote", From 9b112ae7b928068e5b172e65465ac1d0b2093466 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Sat, 28 Oct 2023 01:10:48 -0400 Subject: [PATCH 084/108] Implement dynamic font support `/font//-` (#755) This implements dynamic font protobuf generation, allowing users to request font ranges on the fly, and combining them in any order, e.g. `Font1,Font2,Font3`, same as with sprites and tiles This is a first iteration, without any multithreading support. In theory, this could be done far faster by generating SDFs with multiple threads. ### Current process * during init, figure out all glyphs available in each font, and store them as a bitset * during request: * combine requested bitsets to figure out which glyph should come from which font file * load those glyphs from files (using a single instance of the freetype lib) * convert them to SDFs and package them into a protobuf --------- Co-authored-by: Lucas --- Cargo.lock | 177 +++++++++ Cargo.toml | 2 + debian/config.yaml | 4 + docs/src/21-run-with-cli.md | 5 +- docs/src/30-config-file.md | 6 + docs/src/36-sources-sprites.md | 2 +- docs/src/37-sources-fonts.md | 65 ++++ docs/src/SUMMARY.md | 1 + martin/Cargo.toml | 2 + martin/src/args/root.rs | 9 +- martin/src/config.rs | 12 +- martin/src/fonts/mod.rs | 365 ++++++++++++++++++ martin/src/lib.rs | 1 + martin/src/srv/server.rs | 42 +- martin/src/utils/error.rs | 4 + martin/tests/mb_server_test.rs | 2 + martin/tests/pg_server_test.rs | 1 + martin/tests/pmt_server_test.rs | 2 + tests/config.yaml | 4 + tests/expected/auto/catalog_auto.json | 26 +- tests/expected/configured/catalog_cfg.json | 16 + tests/expected/configured/font_1.pbf | Bin 0 -> 78006 bytes tests/expected/configured/font_2.pbf | Bin 0 -> 79714 bytes tests/expected/configured/font_3.pbf | Bin 0 -> 79714 bytes tests/expected/generated_config.yaml | 4 + tests/expected/given_config.yaml | 3 + .../fixtures/fonts/overpass-mono-regular.ttf | Bin 0 -> 132620 bytes .../fonts/sub_dir/overpass-mono-light.otf | Bin 0 -> 93752 bytes tests/test.sh | 16 +- 29 files changed, 764 insertions(+), 7 deletions(-) create mode 100644 docs/src/37-sources-fonts.md create mode 100644 martin/src/fonts/mod.rs create mode 100644 tests/expected/configured/font_1.pbf create mode 100644 tests/expected/configured/font_2.pbf create mode 100644 tests/expected/configured/font_3.pbf create mode 100755 tests/fixtures/fonts/overpass-mono-regular.ttf create mode 100755 tests/fixtures/fonts/sub_dir/overpass-mono-light.otf diff --git a/Cargo.lock b/Cargo.lock index f868394d6..22b36fc1a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -407,6 +407,21 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bitflags" version = "1.3.2" @@ -1139,6 +1154,27 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "freetype-rs" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59c337e64822dd56a3a83ed75a662a470736bdb3a9fabfb588dff276b94a4e0" +dependencies = [ + "bitflags 1.3.2", + "freetype-sys", + "libc", +] + +[[package]] +name = "freetype-sys" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643148ca6cbad6bec384b52fbe1968547d578c4efe83109e035c43a71734ff88" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "fs4" version = "0.6.6" @@ -1701,6 +1737,7 @@ dependencies = [ "actix-rt", "actix-web", "async-trait", + "bit-set", "brotli", "cargo-husky", "clap", @@ -1718,6 +1755,7 @@ dependencies = [ "martin-mbtiles", "martin-tile-utils", "num_cpus", + "pbf_font_tools", "pmtiles", "postgis", "postgres", @@ -2036,6 +2074,22 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +[[package]] +name = "pbf_font_tools" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67768bb2719d708e2de28cec7271dae35c717122c0fa4d9f8558ef5e7fa83db7" +dependencies = [ + "futures", + "glob", + "protobuf", + "protobuf-codegen", + "protoc-bin-vendored", + "sdf_glyph_renderer", + "thiserror", + "tokio", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -2264,6 +2318,107 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "protobuf" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b65f4a8ec18723a734e5dc09c173e0abf9690432da5340285d536edcb4dac190" +dependencies = [ + "once_cell", + "protobuf-support", + "thiserror", +] + +[[package]] +name = "protobuf-codegen" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e85514a216b1c73111d9032e26cc7a5ecb1bb3d4d9539e91fb72a4395060f78" +dependencies = [ + "anyhow", + "once_cell", + "protobuf", + "protobuf-parse", + "regex", + "tempfile", + "thiserror", +] + +[[package]] +name = "protobuf-parse" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77d6fbd6697c9e531873e81cec565a85e226b99a0f10e1acc079be057fe2fcba" +dependencies = [ + "anyhow", + "indexmap 1.9.3", + "log", + "protobuf", + "protobuf-support", + "tempfile", + "thiserror", + "which", +] + +[[package]] +name = "protobuf-support" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6872f4d4f4b98303239a2b5838f5bbbb77b01ffc892d627957f37a22d7cfe69c" +dependencies = [ + "thiserror", +] + +[[package]] +name = "protoc-bin-vendored" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "005ca8623e5633e298ad1f917d8be0a44bcf406bf3cde3b80e63003e49a3f27d" +dependencies = [ + "protoc-bin-vendored-linux-aarch_64", + "protoc-bin-vendored-linux-ppcle_64", + "protoc-bin-vendored-linux-x86_32", + "protoc-bin-vendored-linux-x86_64", + "protoc-bin-vendored-macos-x86_64", + "protoc-bin-vendored-win32", +] + +[[package]] +name = "protoc-bin-vendored-linux-aarch_64" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fb9fc9cce84c8694b6ea01cc6296617b288b703719b725b8c9c65f7c5874435" + +[[package]] +name = "protoc-bin-vendored-linux-ppcle_64" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d2a07dcf7173a04d49974930ccbfb7fd4d74df30ecfc8762cf2f895a094516" + +[[package]] +name = "protoc-bin-vendored-linux-x86_32" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54fef0b04fcacba64d1d80eed74a20356d96847da8497a59b0a0a436c9165b0" + +[[package]] +name = "protoc-bin-vendored-linux-x86_64" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8782f2ce7d43a9a5c74ea4936f001e9e8442205c244f7a3d4286bd4c37bc924" + +[[package]] +name = "protoc-bin-vendored-macos-x86_64" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5de656c7ee83f08e0ae5b81792ccfdc1d04e7876b1d9a38e6876a9e09e02537" + +[[package]] +name = "protoc-bin-vendored-win32" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9653c3ed92974e34c5a6e0a510864dab979760481714c172e0a34e437cb98804" + [[package]] name = "quote" version = "1.0.33" @@ -2654,6 +2809,16 @@ dependencies = [ "untrusted 0.9.0", ] +[[package]] +name = "sdf_glyph_renderer" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b05c114d181e20b509e03b05856cc5823bc6189d581c276fe37c5ebc5e3b3b9" +dependencies = [ + "freetype-rs", + "thiserror", +] + [[package]] name = "security-framework" version = "2.9.2" @@ -3812,6 +3977,18 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + [[package]] name = "whoami" version = "1.4.1" diff --git a/Cargo.toml b/Cargo.toml index a7ce09837..4c5718548 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ actix-rt = "2" actix-web = "4" anyhow = "1.0" async-trait = "0.1" +bit-set = "0.5.3" brotli = "3" cargo-husky = { version = "1", features = ["user-hooks"], default-features = false } clap = { version = "4", features = ["derive"] } @@ -35,6 +36,7 @@ log = "0.4" martin-mbtiles = { path = "./martin-mbtiles", version = "0.6.0", default-features = false } martin-tile-utils = { path = "./martin-tile-utils", version = "0.1.0" } num_cpus = "1" +pbf_font_tools = { version = "2.5.0", features = ["freetype"] } pmtiles = { version = "0.3", features = ["mmap-async-tokio", "tilejson"] } postgis = "0.9" postgres = { version = "0.19", features = ["with-time-0_3", "with-uuid-1", "with-serde_json-1"] } diff --git a/debian/config.yaml b/debian/config.yaml index d9289352a..f59118cbf 100644 --- a/debian/config.yaml +++ b/debian/config.yaml @@ -29,3 +29,7 @@ worker_processes: 8 # - /path/to/mbtiles.mbtiles # sources: # mb-src1: /path/to/mbtiles1.mbtiles + +# fonts: +# - /path/to/font/file.ttf +# - /path/to/font_dir diff --git a/docs/src/21-run-with-cli.md b/docs/src/21-run-with-cli.md index 3354d91d0..c7c48487a 100644 --- a/docs/src/21-run-with-cli.md +++ b/docs/src/21-run-with-cli.md @@ -19,6 +19,9 @@ Options: -s, --sprite Export a directory with SVG files as a sprite source. Can be specified multiple times + -f, --font + Export a font file or a directory with font files as a font source (recursive). Can be specified multiple times + -k, --keep-alive Connection keep alive timeout. [DEFAULT: 75] @@ -28,7 +31,7 @@ Options: -W, --workers Number of web server workers - -b, --auto-bounds + -b, --auto-bounds Specify how bounds should be computed for the spatial PG tables. [DEFAULT: quick] Possible values: diff --git a/docs/src/30-config-file.md b/docs/src/30-config-file.md index deaeba64d..a4df42845 100644 --- a/docs/src/30-config-file.md +++ b/docs/src/30-config-file.md @@ -183,4 +183,10 @@ sprites: sources: # SVG images in this directory will be published as a "my_sprites" sprite source my_sprites: /path/to/some_dir + +# Font configuration +fonts: + # A list of *.otf, *.ttf, and *.ttc font files and dirs to search recursively. + - /path/to/font/file.ttf + - /path/to/font_dir ``` diff --git a/docs/src/36-sources-sprites.md b/docs/src/36-sources-sprites.md index 76b742028..4cd713996 100644 --- a/docs/src/36-sources-sprites.md +++ b/docs/src/36-sources-sprites.md @@ -1,6 +1,6 @@ ## Sprite Sources -Given a directory with SVG images, Martin will generate a sprite -- a JSON index and a PNG image, for both low and high resolution displays. The SVG filenames without extension will be used as the sprite image IDs. The images are searched recursively in the given directory, so subdirectory names will be used as prefixes for the image IDs, e.g. `icons/bicycle.svg` will be available as `icons/bicycle` sprite image. +Given a directory with SVG images, Martin will generate a sprite -- a JSON index and a PNG image, for both low and high resolution displays. The SVG filenames without extension will be used as the sprite image IDs. The images are searched recursively in the given directory, so subdirectory names will be used as prefixes for the image IDs, e.g. `icons/bicycle.svg` will be available as `icons/bicycle` sprite image. The sprite generation is not yet cached, and may require external reverse proxy or CDN for faster operation. ### API Martin uses [MapLibre sprites API](https://maplibre.org/maplibre-style-spec/sprite/) specification to serve sprites via several endpoints. The sprite image and index are generated on the fly, so if the sprite directory is updated, the changes will be reflected immediately. diff --git a/docs/src/37-sources-fonts.md b/docs/src/37-sources-fonts.md new file mode 100644 index 000000000..797d0b8c2 --- /dev/null +++ b/docs/src/37-sources-fonts.md @@ -0,0 +1,65 @@ +## Font Sources + +Martin can serve glyph ranges from `otf`, `ttf`, and `ttc` fonts as needed by MapLibre text rendering. Martin will generate them dynamically on the fly. +The glyph range generation is not yet cached, and may require external reverse proxy or CDN for faster operation. + +## API +Fonts ranges are available either for a single font, or a combination of multiple fonts. The font names are case-sensitive and should match the font name in the font file as published in the catalog. Make sure to URL-escape font names as they usually contain spaces. + +When combining multiple fonts, the glyph range will contain glyphs from the first listed font if available, and fallback to the next font if the glyph is not available in the first font, etc. The glyph range will be empty if none of the fonts contain the glyph. + +| Type | API | Example | +|----------|------------------------------------------------|--------------------------------------------------------------| +| Single | `/font/{name}/{start}-{end}` | `/font/Overpass%20Mono%20Bold/0-255` | +| Combined | `/font/{name1},{name2},{name_n}/{start}-{end}` | `/font/Overpass%20Mono%20Bold,Overpass%20Mono%20Light/0-255` | + +Martin will show all available fonts at the `/catalog` endpoint. + +```shell +curl http://127.0.0.1:3000/catalog +{ + "fonts": { + "Overpass Mono Bold": { + "family": "Overpass Mono", + "style": "Bold", + "glyphs": 931, + "start": 0, + "end": 64258 + }, + "Overpass Mono Light": { + "family": "Overpass Mono", + "style": "Light", + "glyphs": 931, + "start": 0, + "end": 64258 + }, + "Overpass Mono SemiBold": { + "family": "Overpass Mono", + "style": "SemiBold", + "glyphs": 931, + "start": 0, + "end": 64258 + } + } +} +``` + +## Using from CLI + +A font file or directory can be configured from the [CLI](21-run-with-cli.md) with one or more `--font` parameters. + +```shell +martin --font /path/to/font/file.ttf --font /path/to/font_dir +``` + +## Configuring from Config File + +A font directory can be configured from the config file with the `fonts` key. + +```yaml +# Fonts configuration +fonts: + # A list of *.otf, *.ttf, and *.ttc font files and dirs to search recursively. + - /path/to/font/file.ttf + - /path/to/font_dir +``` diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index b63349be0..018f59937 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -14,6 +14,7 @@ - [MBTiles and PMTiles File Sources](34-sources-files.md) - [Composite Sources](35-sources-composite.md) - [Sprite Sources](36-sources-sprites.md) + - [Font Sources](37-sources-fonts.md) - [Usage and Endpoint API](40-using-endpoints.md) - [Using with MapLibre](41-using-with-maplibre.md) - [Using with Leaflet](42-using-with-leaflet.md) diff --git a/martin/Cargo.toml b/martin/Cargo.toml index 41dce6fcd..08e7d06c2 100644 --- a/martin/Cargo.toml +++ b/martin/Cargo.toml @@ -56,6 +56,7 @@ actix-http.workspace = true actix-rt.workspace = true actix-web.workspace = true async-trait.workspace = true +bit-set.workspace = true brotli.workspace = true clap.workspace = true deadpool-postgres.workspace = true @@ -68,6 +69,7 @@ log.workspace = true martin-mbtiles.workspace = true martin-tile-utils.workspace = true num_cpus.workspace = true +pbf_font_tools.workspace = true pmtiles.workspace = true postgis.workspace = true postgres-protocol.workspace = true diff --git a/martin/src/args/root.rs b/martin/src/args/root.rs index 98c82560b..fe15d2494 100644 --- a/martin/src/args/root.rs +++ b/martin/src/args/root.rs @@ -10,7 +10,7 @@ use crate::args::srv::SrvArgs; use crate::args::State::{Ignore, Share, Take}; use crate::config::Config; use crate::file_config::FileConfigEnum; -use crate::{Error, Result}; +use crate::{Error, OptOneMany, Result}; #[derive(Parser, Debug, PartialEq, Default)] #[command(about, version)] @@ -44,6 +44,9 @@ pub struct MetaArgs { /// Export a directory with SVG files as a sprite source. Can be specified multiple times. #[arg(short, long)] pub sprite: Vec, + /// Export a font file or a directory with font files as a font source (recursive). Can be specified multiple times. + #[arg(short, long)] + pub font: Vec, } impl Args { @@ -81,6 +84,10 @@ impl Args { config.sprites = FileConfigEnum::new(self.meta.sprite); } + if !self.meta.font.is_empty() { + config.fonts = OptOneMany::new(self.meta.font); + } + cli_strings.check() } } diff --git a/martin/src/config.rs b/martin/src/config.rs index 90005d033..625485e01 100644 --- a/martin/src/config.rs +++ b/martin/src/config.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::fs::File; use std::future::Future; use std::io::prelude::*; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::pin::Pin; use futures::future::try_join_all; @@ -10,6 +10,7 @@ use serde::{Deserialize, Serialize}; use subst::VariableMap; use crate::file_config::{resolve_files, FileConfigEnum}; +use crate::fonts::FontSources; use crate::mbtiles::MbtSource; use crate::pg::PgConfig; use crate::pmtiles::PmtSource; @@ -24,6 +25,7 @@ pub type UnrecognizedValues = HashMap; pub struct ServerState { pub tiles: TileSources, pub sprites: SpriteSources, + pub fonts: FontSources, } #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] @@ -43,6 +45,9 @@ pub struct Config { #[serde(default, skip_serializing_if = "FileConfigEnum::is_none")] pub sprites: FileConfigEnum, + #[serde(default, skip_serializing_if = "OptOneMany::is_none")] + pub fonts: OptOneMany, + #[serde(flatten)] pub unrecognized: UnrecognizedValues, } @@ -61,10 +66,14 @@ impl Config { res.extend(self.mbtiles.finalize("mbtiles.")?); res.extend(self.sprites.finalize("sprites.")?); + // TODO: support for unrecognized fonts? + // res.extend(self.fonts.finalize("fonts.")?); + if self.postgres.is_empty() && self.pmtiles.is_empty() && self.mbtiles.is_empty() && self.sprites.is_empty() + && self.fonts.is_empty() { Err(NoSources) } else { @@ -76,6 +85,7 @@ impl Config { Ok(ServerState { tiles: self.resolve_tile_sources(idr).await?, sprites: SpriteSources::resolve(&mut self.sprites)?, + fonts: FontSources::resolve(&mut self.fonts)?, }) } diff --git a/martin/src/fonts/mod.rs b/martin/src/fonts/mod.rs new file mode 100644 index 000000000..f81540dcb --- /dev/null +++ b/martin/src/fonts/mod.rs @@ -0,0 +1,365 @@ +use std::collections::hash_map::Entry; +use std::collections::{BTreeMap, HashMap}; +use std::ffi::OsStr; +use std::fmt::Debug; +use std::path::PathBuf; +use std::sync::OnceLock; + +use bit_set::BitSet; +use itertools::Itertools; +use log::{debug, info, warn}; +use pbf_font_tools::freetype::{Face, Library}; +use pbf_font_tools::protobuf::Message; +use pbf_font_tools::{render_sdf_glyph, Fontstack, Glyphs, PbfFontError}; +use regex::Regex; +use serde::{Deserialize, Serialize}; + +use crate::fonts::FontError::IoError; +use crate::OptOneMany; + +const MAX_UNICODE_CP: usize = 0xFFFF; +const CP_RANGE_SIZE: usize = 256; +const FONT_SIZE: usize = 24; +#[allow(clippy::cast_possible_wrap)] +const CHAR_HEIGHT: isize = (FONT_SIZE as isize) << 6; +const BUFFER_SIZE: usize = 3; +const RADIUS: usize = 8; +const CUTOFF: f64 = 0.25_f64; + +/// Each range is 256 codepoints long, so the highest range ID is 0xFFFF / 256 = 255. +const MAX_UNICODE_CP_RANGE_ID: usize = MAX_UNICODE_CP / CP_RANGE_SIZE; + +#[derive(thiserror::Error, Debug)] +pub enum FontError { + #[error("Font {0} not found")] + FontNotFound(String), + + #[error("Font range start ({0}) must be <= end ({1})")] + InvalidFontRangeStartEnd(u32, u32), + + #[error("Font range start ({0}) must be multiple of {CP_RANGE_SIZE} (e.g. 0, 256, 512, ...)")] + InvalidFontRangeStart(u32), + + #[error( + "Font range end ({0}) must be multiple of {CP_RANGE_SIZE} - 1 (e.g. 255, 511, 767, ...)" + )] + InvalidFontRangeEnd(u32), + + #[error("Given font range {0}-{1} is invalid. It must be {CP_RANGE_SIZE} characters long (e.g. 0-255, 256-511, ...)")] + InvalidFontRange(u32, u32), + + #[error("FreeType font error: {0}")] + FreeType(#[from] pbf_font_tools::freetype::Error), + + #[error("IO error accessing {}: {0}", .1.display())] + IoError(std::io::Error, PathBuf), + + #[error("Invalid font file {}", .0.display())] + InvalidFontFilePath(PathBuf), + + #[error("No font files found in {}", .0.display())] + NoFontFilesFound(PathBuf), + + #[error("Font {} could not be loaded", .0.display())] + UnableToReadFont(PathBuf), + + #[error("{0} in file {}", .1.display())] + FontProcessingError(spreet::error::Error, PathBuf), + + #[error("Font {0} is missing a family name")] + MissingFamilyName(PathBuf), + + #[error("PBF Font error: {0}")] + PbfFontError(#[from] PbfFontError), + + #[error("Error serializing protobuf: {0}")] + ErrorSerializingProtobuf(#[from] pbf_font_tools::protobuf::Error), +} + +type GetGlyphInfo = (BitSet, usize, Vec<(usize, usize)>, usize, usize); + +fn get_available_codepoints(face: &mut Face) -> Option { + let mut codepoints = BitSet::with_capacity(MAX_UNICODE_CP); + let mut spans = Vec::new(); + let mut first: Option = None; + let mut count = 0; + + for cp in 0..=MAX_UNICODE_CP { + if face.get_char_index(cp) != 0 { + codepoints.insert(cp); + count += 1; + if first.is_none() { + first = Some(cp); + } + } else if let Some(start) = first { + spans.push((start, cp - 1)); + first = None; + } + } + + if count == 0 { + None + } else { + let start = spans[0].0; + let end = spans[spans.len() - 1].1; + Some((codepoints, count, spans, start, end)) + } +} + +#[derive(Debug, Clone, Default)] +pub struct FontSources { + fonts: HashMap, + masks: Vec, +} + +pub type FontCatalog = BTreeMap; + +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] +pub struct CatalogFontEntry { + pub family: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub style: Option, + pub glyphs: usize, + pub start: usize, + pub end: usize, +} + +impl FontSources { + pub fn resolve(config: &mut OptOneMany) -> Result { + if config.is_empty() { + return Ok(Self::default()); + } + + let mut fonts = HashMap::new(); + let lib = Library::init()?; + + for path in config.iter() { + recurse_dirs(&lib, path.clone(), &mut fonts, true)?; + } + + let mut masks = Vec::with_capacity(MAX_UNICODE_CP_RANGE_ID + 1); + + let mut bs = BitSet::with_capacity(CP_RANGE_SIZE); + for v in 0..=MAX_UNICODE_CP { + bs.insert(v); + if v % CP_RANGE_SIZE == (CP_RANGE_SIZE - 1) { + masks.push(bs); + bs = BitSet::with_capacity(CP_RANGE_SIZE); + } + } + + Ok(Self { fonts, masks }) + } + + #[must_use] + pub fn get_catalog(&self) -> FontCatalog { + self.fonts + .iter() + .map(|(k, v)| (k.clone(), v.catalog_entry.clone())) + .sorted_by(|(a, _), (b, _)| a.cmp(b)) + .collect() + } + + /// Given a list of IDs in a format "id1,id2,id3", return a combined font. + #[allow(clippy::cast_possible_truncation)] + pub fn get_font_range(&self, ids: &str, start: u32, end: u32) -> Result, FontError> { + if start > end { + return Err(FontError::InvalidFontRangeStartEnd(start, end)); + } + if start % (CP_RANGE_SIZE as u32) != 0 { + return Err(FontError::InvalidFontRangeStart(start)); + } + if end % (CP_RANGE_SIZE as u32) != (CP_RANGE_SIZE as u32 - 1) { + return Err(FontError::InvalidFontRangeEnd(end)); + } + if (end - start) != (CP_RANGE_SIZE as u32 - 1) { + return Err(FontError::InvalidFontRange(start, end)); + } + + let mut needed = self.masks[(start as usize) / CP_RANGE_SIZE].clone(); + let fonts = ids + .split(',') + .filter_map(|id| match self.fonts.get(id) { + None => Some(Err(FontError::FontNotFound(id.to_string()))), + Some(v) => { + let mut ds = needed.clone(); + ds.intersect_with(&v.codepoints); + if ds.is_empty() { + None + } else { + needed.difference_with(&v.codepoints); + Some(Ok((id, v, ds))) + } + } + }) + .collect::, FontError>>()?; + + if fonts.is_empty() { + return Ok(Vec::new()); + } + + let lib = Library::init()?; + let mut stack = Fontstack::new(); + + for (id, font, ds) in fonts { + if stack.has_name() { + let name = stack.mut_name(); + name.push_str(", "); + name.push_str(id); + } else { + stack.set_name(id.to_string()); + } + + let face = lib.new_face(&font.path, font.face_index)?; + + // FreeType conventions: char width or height of zero means "use the same value" + // and setting both resolution values to zero results in the default value + // of 72 dpi. + // + // See https://www.freetype.org/freetype2/docs/reference/ft2-base_interface.html#ft_set_char_size + // and https://www.freetype.org/freetype2/docs/tutorial/step1.html for details. + face.set_char_size(0, CHAR_HEIGHT, 0, 0)?; + + for cp in &ds { + let glyph = render_sdf_glyph(&face, cp as u32, BUFFER_SIZE, RADIUS, CUTOFF)?; + stack.glyphs.push(glyph); + } + } + + stack.set_range(format!("{start}-{end}")); + + let mut glyphs = Glyphs::new(); + glyphs.stacks.push(stack); + let mut result = Vec::new(); + glyphs.write_to_vec(&mut result)?; + Ok(result) + } +} + +#[derive(Clone, Debug)] +pub struct FontSource { + path: PathBuf, + face_index: isize, + codepoints: BitSet, + catalog_entry: CatalogFontEntry, +} + +fn recurse_dirs( + lib: &Library, + path: PathBuf, + fonts: &mut HashMap, + is_top_level: bool, +) -> Result<(), FontError> { + let start_count = fonts.len(); + if path.is_dir() { + for dir_entry in path + .read_dir() + .map_err(|e| IoError(e, path.clone()))? + .flatten() + { + recurse_dirs(lib, dir_entry.path(), fonts, false)?; + } + if is_top_level && fonts.len() == start_count { + return Err(FontError::NoFontFilesFound(path)); + } + } else { + if path + .extension() + .and_then(OsStr::to_str) + .is_some_and(|e| ["otf", "ttf", "ttc"].contains(&e)) + { + parse_font(lib, fonts, path.clone())?; + } + if is_top_level && fonts.len() == start_count { + return Err(FontError::InvalidFontFilePath(path)); + } + } + + Ok(()) +} + +fn parse_font( + lib: &Library, + fonts: &mut HashMap, + path: PathBuf, +) -> Result<(), FontError> { + static RE_SPACES: OnceLock = OnceLock::new(); + + let mut face = lib.new_face(&path, 0)?; + let num_faces = face.num_faces() as isize; + for face_index in 0..num_faces { + if face_index > 0 { + face = lib.new_face(&path, face_index)?; + } + let Some(family) = face.family_name() else { + return Err(FontError::MissingFamilyName(path)); + }; + let mut name = family.clone(); + let style = face.style_name(); + if let Some(style) = &style { + name.push(' '); + name.push_str(style); + } + // Make sure font name has no slashes or commas, replacing them with spaces and de-duplicating spaces + name = name.replace(['/', ','], " "); + name = RE_SPACES + .get_or_init(|| Regex::new(r"\s+").unwrap()) + .replace_all(name.as_str(), " ") + .to_string(); + + match fonts.entry(name) { + Entry::Occupied(v) => { + warn!( + "Ignoring duplicate font {} from {} because it was already configured from {}", + v.key(), + path.display(), + v.get().path.display() + ); + } + Entry::Vacant(v) => { + let key = v.key(); + let Some((codepoints, glyphs, ranges, start, end)) = + get_available_codepoints(&mut face) + else { + warn!( + "Ignoring font {key} from {} because it has no available glyphs", + path.display() + ); + continue; + }; + + info!( + "Configured font {key} with {glyphs} glyphs ({start:04X}-{end:04X}) from {}", + path.display() + ); + debug!( + "Available font ranges: {}", + ranges + .iter() + .map(|(s, e)| if s == e { + format!("{s:02X}") + } else { + format!("{s:02X}-{e:02X}") + }) + .collect::>() + .join(", "), + ); + + v.insert(FontSource { + path: path.clone(), + face_index, + codepoints, + catalog_entry: CatalogFontEntry { + family, + style, + glyphs, + start, + end, + }, + }); + } + } + } + + Ok(()) +} diff --git a/martin/src/lib.rs b/martin/src/lib.rs index 8903799d0..827fa036a 100644 --- a/martin/src/lib.rs +++ b/martin/src/lib.rs @@ -11,6 +11,7 @@ pub mod args; mod config; pub mod file_config; +pub mod fonts; pub mod mbtiles; pub mod pg; pub mod pmtiles; diff --git a/martin/src/srv/server.rs b/martin/src/srv/server.rs index e6c740d85..df755853e 100755 --- a/martin/src/srv/server.rs +++ b/martin/src/srv/server.rs @@ -24,6 +24,7 @@ use serde::{Deserialize, Serialize}; use tilejson::{tilejson, TileJSON}; use crate::config::ServerState; +use crate::fonts::{FontCatalog, FontError, FontSources}; use crate::source::{Source, TileCatalog, TileSources, UrlQuery}; use crate::sprites::{SpriteCatalog, SpriteError, SpriteSources}; use crate::srv::config::{SrvConfig, KEEP_ALIVE_DEFAULT, LISTEN_ADDRESSES_DEFAULT}; @@ -49,6 +50,7 @@ static SUPPORTED_ENCODINGS: &[HeaderEnc] = &[ pub struct Catalog { pub tiles: TileCatalog, pub sprites: SpriteCatalog, + pub fonts: FontCatalog, } impl Catalog { @@ -56,6 +58,7 @@ impl Catalog { Ok(Self { tiles: state.tiles.get_catalog(), sprites: state.sprites.get_catalog()?, + fonts: state.fonts.get_catalog(), }) } } @@ -86,6 +89,19 @@ pub fn map_sprite_error(e: SpriteError) -> actix_web::Error { } } +pub fn map_font_error(e: FontError) -> actix_web::Error { + #[allow(clippy::enum_glob_use)] + use FontError::*; + match e { + FontNotFound(_) => ErrorNotFound(e.to_string()), + InvalidFontRangeStartEnd(_, _) + | InvalidFontRangeStart(_) + | InvalidFontRangeEnd(_) + | InvalidFontRange(_, _) => ErrorBadRequest(e.to_string()), + _ => map_internal_error(e), + } +} + /// Root path will eventually have a web front. For now, just a stub. #[route("/", method = "GET", method = "HEAD")] #[allow(clippy::unused_async)] @@ -147,6 +163,28 @@ async fn get_sprite_json( Ok(HttpResponse::Ok().json(sheet.get_index())) } +#[derive(Deserialize, Debug)] +struct FontRequest { + fontstack: String, + start: u32, + end: u32, +} + +#[route( + "/font/{fontstack}/{start}-{end}", + method = "GET", + wrap = "middleware::Compress::default()" +)] +#[allow(clippy::unused_async)] +async fn get_font(path: Path, fonts: Data) -> Result { + let data = fonts + .get_font_range(&path.fontstack, path.start, path.end) + .map_err(map_font_error)?; + Ok(HttpResponse::Ok() + .content_type("application/x-protobuf") + .body(data)) +} + #[route( "/{source_ids}", method = "GET", @@ -427,7 +465,8 @@ pub fn router(cfg: &mut web::ServiceConfig) { .service(git_source_info) .service(get_tile) .service(get_sprite_json) - .service(get_sprite_png); + .service(get_sprite_png) + .service(get_font); } /// Create a new initialized Actix `App` instance together with the listening address. @@ -447,6 +486,7 @@ pub fn new_server(config: SrvConfig, state: ServerState) -> crate::Result<(Serve App::new() .app_data(Data::new(state.tiles.clone())) .app_data(Data::new(state.sprites.clone())) + .app_data(Data::new(state.fonts.clone())) .app_data(Data::new(catalog.clone())) .wrap(cors_middleware) .wrap(middleware::NormalizePath::new(TrailingSlash::MergeOnly)) diff --git a/martin/src/utils/error.rs b/martin/src/utils/error.rs index a06ddaf6a..bc28e4efc 100644 --- a/martin/src/utils/error.rs +++ b/martin/src/utils/error.rs @@ -3,6 +3,7 @@ use std::io; use std::path::PathBuf; use crate::file_config::FileError; +use crate::fonts::FontError; use crate::pg::PgError; use crate::sprites::SpriteError; @@ -59,4 +60,7 @@ pub enum Error { #[error("{0}")] SpriteError(#[from] SpriteError), + + #[error("{0}")] + FontError(#[from] FontError), } diff --git a/martin/tests/mb_server_test.rs b/martin/tests/mb_server_test.rs index 31f3177ef..046920f24 100644 --- a/martin/tests/mb_server_test.rs +++ b/martin/tests/mb_server_test.rs @@ -69,6 +69,7 @@ async fn mbt_get_catalog() { content_type: image/webp name: ne2sr sprites: {} + fonts: {} "###); } @@ -100,6 +101,7 @@ async fn mbt_get_catalog_gzip() { content_type: image/webp name: ne2sr sprites: {} + fonts: {} "###); } diff --git a/martin/tests/pg_server_test.rs b/martin/tests/pg_server_test.rs index 341e9aabc..3a48728a3 100644 --- a/martin/tests/pg_server_test.rs +++ b/martin/tests/pg_server_test.rs @@ -115,6 +115,7 @@ postgres: content_type: application/x-protobuf description: public.table_source_multiple_geom.geom2 sprites: {} + fonts: {} "###); } diff --git a/martin/tests/pmt_server_test.rs b/martin/tests/pmt_server_test.rs index 3ed778313..b9f89628d 100644 --- a/martin/tests/pmt_server_test.rs +++ b/martin/tests/pmt_server_test.rs @@ -54,6 +54,7 @@ async fn pmt_get_catalog() { stamen_toner__raster_CC-BY-ODbL_z3: content_type: image/png sprites: {} + fonts: {} "###); } @@ -72,6 +73,7 @@ async fn pmt_get_catalog_gzip() { p_png: content_type: image/png sprites: {} + fonts: {} "###); } diff --git a/tests/config.yaml b/tests/config.yaml index 471f31428..cecc48aa7 100644 --- a/tests/config.yaml +++ b/tests/config.yaml @@ -168,3 +168,7 @@ sprites: paths: tests/fixtures/sprites/src1 sources: mysrc: tests/fixtures/sprites/src2 + +fonts: + - tests/fixtures/fonts/overpass-mono-regular.ttf + - tests/fixtures/fonts diff --git a/tests/expected/auto/catalog_auto.json b/tests/expected/auto/catalog_auto.json index 3ce4162f5..fd502f77f 100644 --- a/tests/expected/auto/catalog_auto.json +++ b/tests/expected/auto/catalog_auto.json @@ -163,5 +163,29 @@ "description": "Major cities from Natural Earth data" } }, - "sprites": {} + "sprites": { + "src1": { + "images": [ + "another_bicycle", + "bear", + "sub/circle" + ] + } + }, + "fonts": { + "Overpass Mono Light": { + "family": "Overpass Mono", + "style": "Light", + "glyphs": 931, + "start": 0, + "end": 64258 + }, + "Overpass Mono Regular": { + "family": "Overpass Mono", + "style": "Regular", + "glyphs": 931, + "start": 0, + "end": 64258 + } + } } diff --git a/tests/expected/configured/catalog_cfg.json b/tests/expected/configured/catalog_cfg.json index 2fb48ab5d..810687e76 100644 --- a/tests/expected/configured/catalog_cfg.json +++ b/tests/expected/configured/catalog_cfg.json @@ -53,5 +53,21 @@ "sub/circle" ] } + }, + "fonts": { + "Overpass Mono Light": { + "family": "Overpass Mono", + "style": "Light", + "glyphs": 931, + "start": 0, + "end": 64258 + }, + "Overpass Mono Regular": { + "family": "Overpass Mono", + "style": "Regular", + "glyphs": 931, + "start": 0, + "end": 64258 + } } } diff --git a/tests/expected/configured/font_1.pbf b/tests/expected/configured/font_1.pbf new file mode 100644 index 0000000000000000000000000000000000000000..bb3447323326210290a8245565925ffa8be32993 GIT binary patch literal 78006 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 JXH9s3e*?wj6U+br literal 0 HcmV?d00001 diff --git a/tests/expected/configured/font_2.pbf b/tests/expected/configured/font_2.pbf new file mode 100644 index 0000000000000000000000000000000000000000..f57bcc48cbad0691b4f71030c97dc7ddb0614d40 GIT binary patch literal 79714 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 #a3c4B=UT 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 MUy7XgcEAOH2mHh-asU7T literal 0 HcmV?d00001 diff --git a/tests/expected/configured/font_3.pbf b/tests/expected/configured/font_3.pbf new file mode 100644 index 0000000000000000000000000000000000000000..f57bcc48cbad0691b4f71030c97dc7ddb0614d40 GIT binary patch literal 79714 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 #a3c4B=UT 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 MUy7XgcEAOH2mHh-asU7T literal 0 HcmV?d00001 diff --git a/tests/expected/generated_config.yaml b/tests/expected/generated_config.yaml index 50bf43f96..435d52f41 100644 --- a/tests/expected/generated_config.yaml +++ b/tests/expected/generated_config.yaml @@ -212,3 +212,7 @@ mbtiles: world_cities_diff: tests/fixtures/mbtiles/world_cities_diff.mbtiles world_cities_modified: tests/fixtures/mbtiles/world_cities_modified.mbtiles zoomed_world_cities: tests/fixtures/mbtiles/zoomed_world_cities.mbtiles +sprites: tests/fixtures/sprites/src1 +fonts: +- tests/fixtures/fonts/overpass-mono-regular.ttf +- tests/fixtures/fonts diff --git a/tests/expected/given_config.yaml b/tests/expected/given_config.yaml index 6ba51282e..64291515e 100644 --- a/tests/expected/given_config.yaml +++ b/tests/expected/given_config.yaml @@ -164,3 +164,6 @@ sprites: paths: tests/fixtures/sprites/src1 sources: mysrc: tests/fixtures/sprites/src2 +fonts: +- tests/fixtures/fonts/overpass-mono-regular.ttf +- tests/fixtures/fonts diff --git a/tests/fixtures/fonts/overpass-mono-regular.ttf b/tests/fixtures/fonts/overpass-mono-regular.ttf new file mode 100755 index 0000000000000000000000000000000000000000..107fe320d187c03de47e42121885946d04b3c2a0 GIT binary patch literal 132620 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
}JmjU@4etf$G4vbI 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 z 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 literal 0 HcmV?d00001 diff --git a/tests/test.sh b/tests/test.sh index 2ff631790..e620e19ee 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -97,6 +97,15 @@ test_png() fi } +test_font() +{ + FILENAME="$TEST_OUT_DIR/$1.pbf" + URL="$MARTIN_URL/$2" + + echo "Testing $(basename "$FILENAME") from $URL" + $CURL "$URL" > "$FILENAME" +} + # Delete a line from a file $1 that matches parameter $2 remove_line() { @@ -127,6 +136,7 @@ validate_log() # Make sure the log has just the expected warnings, remove them, and test that there are no other ones test_log_has_str "$LOG_FILE" 'WARN martin::pg::table_source] Table public.table_source has no spatial index on column geom' + test_log_has_str "$LOG_FILE" 'WARN martin::fonts] Ignoring duplicate font Overpass Mono Regular from tests/fixtures/fonts/overpass-mono-regular.ttf because it was already configured from tests/fixtures/fonts/overpass-mono-regular.ttf' echo "Checking for no other warnings or errors in the log" if grep -e ' ERROR ' -e ' WARN ' "$LOG_FILE"; then @@ -153,7 +163,7 @@ echo "Test auto configured Martin" TEST_OUT_DIR="$(dirname "$0")/output/auto" mkdir -p "$TEST_OUT_DIR" -ARG=(--default-srid 900913 --auto-bounds calc --save-config "$(dirname "$0")/output/generated_config.yaml" tests/fixtures/mbtiles tests/fixtures/pmtiles) +ARG=(--default-srid 900913 --auto-bounds calc --save-config "$(dirname "$0")/output/generated_config.yaml" tests/fixtures/mbtiles tests/fixtures/pmtiles --sprite tests/fixtures/sprites/src1 --font tests/fixtures/fonts/overpass-mono-regular.ttf --font tests/fixtures/fonts) set -x $MARTIN_BIN "${ARG[@]}" 2>&1 | tee "${TMP_DIR}/test_log_1.txt" & PROCESS_ID=`jobs -p` @@ -268,6 +278,10 @@ test_png spr_cmp sprite/src1,mysrc.png test_jsn spr_cmp_2x sprite/src1,mysrc@2x.json test_png spr_cmp_2x sprite/src1,mysrc@2x.png +test_font font_1 font/Overpass%20Mono%20Light/0-255 +test_font font_2 font/Overpass%20Mono%20Regular/0-255 +test_font font_3 font/Overpass%20Mono%20Regular,Overpass%20Mono%20Light/0-255 + kill_process $PROCESS_ID validate_log "${TMP_DIR}/test_log_2.txt" From 8b274f49f6bd37c5b538806c83e8009090ae5d21 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Sat, 28 Oct 2023 01:23:36 -0400 Subject: [PATCH 085/108] save err for all platforms --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8826b5abd..669fb56c4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -359,10 +359,10 @@ jobs: env: DATABASE_URL: ${{ steps.pg.outputs.connection-uri }} - name: Save test output on failure (Linux) - if: failure() && matrix.target == 'x86_64-unknown-linux-gnu' + if: failure() uses: actions/upload-artifact@v3 with: - name: failed-test-output + name: failed-test-output-${{ runner.os }} path: tests/output/* retention-days: 5 From 0178ca731d474e95e81725ea6f137282adec8205 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Sat, 28 Oct 2023 02:06:37 -0400 Subject: [PATCH 086/108] better logging on failure --- .github/workflows/ci.yml | 10 +++++++--- tests/test.sh | 12 ++++++------ 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 669fb56c4..49a9c9871 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -358,12 +358,14 @@ jobs: tests/test.sh env: DATABASE_URL: ${{ steps.pg.outputs.connection-uri }} - - name: Save test output on failure (Linux) + - name: Save test output on failure if: failure() uses: actions/upload-artifact@v3 with: name: failed-test-output-${{ runner.os }} - path: tests/output/* + path: | + tests/output/* + target/test_logs/* retention-days: 5 test-with-svc: @@ -489,7 +491,9 @@ jobs: uses: actions/upload-artifact@v3 with: name: test-output - path: tests/output/* + path: | + tests/output/* + target/test_logs/* retention-days: 5 package: diff --git a/tests/test.sh b/tests/test.sh index e620e19ee..4e3ce4601 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -13,8 +13,8 @@ MARTIN_BIN="${MARTIN_BIN:-cargo run --} ${MARTIN_ARGS}" MBTILES_BUILD="${MBTILES_BUILD:-cargo build -p martin-mbtiles}" MBTILES_BIN="${MBTILES_BIN:-target/debug/mbtiles}" -TMP_DIR="${TMP_DIR:-target/tmp}" -mkdir -p "$TMP_DIR" +LOG_DIR="${LOG_DIR:-target/test_logs}" +mkdir -p "$LOG_DIR" function wait_for_martin { # Seems the --retry-all-errors option is not available on older curl versions, but maybe in the future we can just use this: @@ -165,7 +165,7 @@ mkdir -p "$TEST_OUT_DIR" ARG=(--default-srid 900913 --auto-bounds calc --save-config "$(dirname "$0")/output/generated_config.yaml" tests/fixtures/mbtiles tests/fixtures/pmtiles --sprite tests/fixtures/sprites/src1 --font tests/fixtures/fonts/overpass-mono-regular.ttf --font tests/fixtures/fonts) set -x -$MARTIN_BIN "${ARG[@]}" 2>&1 | tee "${TMP_DIR}/test_log_1.txt" & +$MARTIN_BIN "${ARG[@]}" 2>&1 | tee "${LOG_DIR}/test_log_1.txt" & PROCESS_ID=`jobs -p` { set +x; } 2> /dev/null @@ -240,7 +240,7 @@ test_pbf mb_mvt_2_3_1 world_cities/2/3/1 test_pbf points_empty_srid_0_0_0 points_empty_srid/0/0/0 kill_process $PROCESS_ID -validate_log "${TMP_DIR}/test_log_1.txt" +validate_log "${LOG_DIR}/test_log_1.txt" echo "------------------------------------------------------------------------------------------------------------------------" @@ -250,7 +250,7 @@ mkdir -p "$TEST_OUT_DIR" ARG=(--config tests/config.yaml --max-feature-count 1000 --save-config "$(dirname "$0")/output/given_config.yaml" -W 1) set -x -$MARTIN_BIN "${ARG[@]}" 2>&1 | tee "${TMP_DIR}/test_log_2.txt" & +$MARTIN_BIN "${ARG[@]}" 2>&1 | tee "${LOG_DIR}/test_log_2.txt" & PROCESS_ID=`jobs -p` { set +x; } 2> /dev/null trap "kill -9 $PROCESS_ID 2> /dev/null || true" EXIT @@ -283,7 +283,7 @@ test_font font_2 font/Overpass%20Mono%20Regular/0-255 test_font font_3 font/Overpass%20Mono%20Regular,Overpass%20Mono%20Light/0-255 kill_process $PROCESS_ID -validate_log "${TMP_DIR}/test_log_2.txt" +validate_log "${LOG_DIR}/test_log_2.txt" remove_line "$(dirname "$0")/output/given_config.yaml" " connection_string: " remove_line "$(dirname "$0")/output/generated_config.yaml" " connection_string: " From 76a1b7fb3315ea488aabc68eb63629f368886cb4 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Sat, 28 Oct 2023 04:01:48 -0400 Subject: [PATCH 087/108] fix CI --- martin/src/fonts/mod.rs | 3 +-- tests/test.sh | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/martin/src/fonts/mod.rs b/martin/src/fonts/mod.rs index f81540dcb..5f286e441 100644 --- a/martin/src/fonts/mod.rs +++ b/martin/src/fonts/mod.rs @@ -301,9 +301,8 @@ fn parse_font( name.push_str(style); } // Make sure font name has no slashes or commas, replacing them with spaces and de-duplicating spaces - name = name.replace(['/', ','], " "); name = RE_SPACES - .get_or_init(|| Regex::new(r"\s+").unwrap()) + .get_or_init(|| Regex::new(r"(\s|/|,)+").unwrap()) .replace_all(name.as_str(), " ") .to_string(); diff --git a/tests/test.sh b/tests/test.sh index 4e3ce4601..ec3dbb2dc 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -136,7 +136,7 @@ validate_log() # Make sure the log has just the expected warnings, remove them, and test that there are no other ones test_log_has_str "$LOG_FILE" 'WARN martin::pg::table_source] Table public.table_source has no spatial index on column geom' - test_log_has_str "$LOG_FILE" 'WARN martin::fonts] Ignoring duplicate font Overpass Mono Regular from tests/fixtures/fonts/overpass-mono-regular.ttf because it was already configured from tests/fixtures/fonts/overpass-mono-regular.ttf' + test_log_has_str "$LOG_FILE" 'WARN martin::fonts] Ignoring duplicate font Overpass Mono Regular from tests' echo "Checking for no other warnings or errors in the log" if grep -e ' ERROR ' -e ' WARN ' "$LOG_FILE"; then From 47b3106ac6d148cfe244f6df49832e5964bce877 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Sat, 28 Oct 2023 04:50:49 -0400 Subject: [PATCH 088/108] Rename martin-mbtiles crate to mbtiles (#976) This simplifies usage as both a crate and as a tool --- .github/workflows/ci.yml | 12 ++++++------ Cargo.lock | 18 +++++++++--------- Cargo.toml | 4 ++-- docs/src/50-tools.md | 2 +- justfile | 8 ++++---- martin/Cargo.toml | 2 +- martin/src/mbtiles/mod.rs | 2 +- {martin-mbtiles => mbtiles}/.env | 0 ...1a167b406de9961ac3cc69649c6152a6d7a9b7.json | 0 ...9de564de47dde1d588f17e68ec58115ac73a39.json | 0 ...9d23bd852ba445c1058aed380fe83bed618c29.json | 0 ...b05f2a7999788167f41c685af3ca6f5a1359f4.json | 0 ...a6e16068c9897dd25d0ebd32929db9960596b4.json | 0 ...febc59c27dbf204d09a9c1fb0b3bf9aaad284b.json | 0 ...48c3a379e96acbdf5fc52d14e29bc726fefab7.json | 0 ...e8098085994c8fba93e0293359afd43079c50c.json | 0 ...2f8fe1b1e7b6fb87c4e04ad7406a2bbfd35bec.json | 0 ...3aaca18373bbdd548a8378ae7fbeed351b4b87.json | 0 ...3ba30fbf018805fe9ca2acd2b2e225183d1f13.json | 0 ...b22ea61633c21afb45d3d2b9aeec068d72cce0.json | 0 ...f007c66a1cc45bdfcdc38f93d6ba759125a9aa.json | 0 ...b2cd90903d5401f8f0956245e5163bedd23a4d.json | 0 ...4ad8c695c4c2350f294aefd210eccec603d905.json | 0 {martin-mbtiles => mbtiles}/Cargo.toml | 4 ++-- {martin-mbtiles => mbtiles}/README.md | 8 ++++---- {martin-mbtiles => mbtiles}/src/bin/main.rs | 4 ++-- {martin-mbtiles => mbtiles}/src/copier.rs | 0 {martin-mbtiles => mbtiles}/src/errors.rs | 0 {martin-mbtiles => mbtiles}/src/lib.rs | 0 {martin-mbtiles => mbtiles}/src/mbtiles.rs | 0 {martin-mbtiles => mbtiles}/src/patcher.rs | 0 {martin-mbtiles => mbtiles}/src/pool.rs | 0 {martin-mbtiles => mbtiles}/src/queries.rs | 0 {martin-mbtiles => mbtiles}/tests/mbtiles.rs | 8 +++----- .../mbtiles__convert@v1__z6__flat-flat.snap | 2 +- .../mbtiles__convert@v1__z6__flat-hash.snap | 2 +- .../mbtiles__convert@v1__z6__flat-norm.snap | 2 +- .../mbtiles__convert@v1__z6__hash-flat.snap | 2 +- .../mbtiles__convert@v1__z6__hash-hash.snap | 2 +- .../mbtiles__convert@v1__z6__hash-norm.snap | 2 +- .../mbtiles__convert@v1__z6__norm-flat.snap | 2 +- .../mbtiles__convert@v1__z6__norm-hash.snap | 2 +- .../mbtiles__convert@v1__z6__norm-norm.snap | 2 +- .../mbtiles__databases@flat__dif.snap | 2 +- .../mbtiles__databases@flat__v1-no-hash.snap | 2 +- .../snapshots/mbtiles__databases@flat__v1.snap | 2 +- .../snapshots/mbtiles__databases@flat__v2.snap | 2 +- .../mbtiles__databases@hash__dif.snap | 2 +- .../mbtiles__databases@hash__v1-no-hash.snap | 2 +- .../snapshots/mbtiles__databases@hash__v1.snap | 2 +- .../snapshots/mbtiles__databases@hash__v2.snap | 2 +- .../mbtiles__databases@norm__dif.snap | 2 +- .../mbtiles__databases@norm__v1-no-hash.snap | 2 +- .../snapshots/mbtiles__databases@norm__v1.snap | 2 +- .../snapshots/mbtiles__databases@norm__v2.snap | 2 +- tests/test.sh | 2 +- 56 files changed, 57 insertions(+), 59 deletions(-) rename {martin-mbtiles => mbtiles}/.env (100%) rename {martin-mbtiles => mbtiles}/.sqlx/query-0a4540e8c33c71222a68ff5ecc1a167b406de9961ac3cc69649c6152a6d7a9b7.json (100%) rename {martin-mbtiles => mbtiles}/.sqlx/query-176e99c6945b0789119d0d21a99de564de47dde1d588f17e68ec58115ac73a39.json (100%) rename {martin-mbtiles => mbtiles}/.sqlx/query-386a375cf65c3e5aef51deffc99d23bd852ba445c1058aed380fe83bed618c29.json (100%) rename {martin-mbtiles => mbtiles}/.sqlx/query-428a035a55a07cbb9daac42c3ab05f2a7999788167f41c685af3ca6f5a1359f4.json (100%) rename {martin-mbtiles => mbtiles}/.sqlx/query-4d952966a8d8a030d2467c0701a6e16068c9897dd25d0ebd32929db9960596b4.json (100%) rename {martin-mbtiles => mbtiles}/.sqlx/query-5b298df51dccbf0d8a22433a99febc59c27dbf204d09a9c1fb0b3bf9aaad284b.json (100%) rename {martin-mbtiles => mbtiles}/.sqlx/query-60264fa07915878b3f7ba0067f48c3a379e96acbdf5fc52d14e29bc726fefab7.json (100%) rename {martin-mbtiles => mbtiles}/.sqlx/query-7341bfc10beb4719811556a57ae8098085994c8fba93e0293359afd43079c50c.json (100%) rename {martin-mbtiles => mbtiles}/.sqlx/query-748436831449877b242d6e167a2f8fe1b1e7b6fb87c4e04ad7406a2bbfd35bec.json (100%) rename {martin-mbtiles => mbtiles}/.sqlx/query-77b2f46851c4e991230ec6a5d33aaca18373bbdd548a8378ae7fbeed351b4b87.json (100%) rename {martin-mbtiles => mbtiles}/.sqlx/query-809e89c3b223e28c6716d405e13ba30fbf018805fe9ca2acd2b2e225183d1f13.json (100%) rename {martin-mbtiles => mbtiles}/.sqlx/query-85b46712c445679053e768cc98b22ea61633c21afb45d3d2b9aeec068d72cce0.json (100%) rename {martin-mbtiles => mbtiles}/.sqlx/query-c8ef3dc53f1f6fd80e266aab2bf007c66a1cc45bdfcdc38f93d6ba759125a9aa.json (100%) rename {martin-mbtiles => mbtiles}/.sqlx/query-d6ac76a234c97d0dc1fc4331d8b2cd90903d5401f8f0956245e5163bedd23a4d.json (100%) rename {martin-mbtiles => mbtiles}/.sqlx/query-f547ff198e3bb604550a3f191e4ad8c695c4c2350f294aefd210eccec603d905.json (100%) rename {martin-mbtiles => mbtiles}/Cargo.toml (97%) rename {martin-mbtiles => mbtiles}/README.md (84%) rename {martin-mbtiles => mbtiles}/src/bin/main.rs (98%) rename {martin-mbtiles => mbtiles}/src/copier.rs (100%) rename {martin-mbtiles => mbtiles}/src/errors.rs (100%) rename {martin-mbtiles => mbtiles}/src/lib.rs (100%) rename {martin-mbtiles => mbtiles}/src/mbtiles.rs (100%) rename {martin-mbtiles => mbtiles}/src/patcher.rs (100%) rename {martin-mbtiles => mbtiles}/src/pool.rs (100%) rename {martin-mbtiles => mbtiles}/src/queries.rs (100%) rename {martin-mbtiles => mbtiles}/tests/mbtiles.rs (98%) rename {martin-mbtiles => mbtiles}/tests/snapshots/mbtiles__convert@v1__z6__flat-flat.snap (95%) rename {martin-mbtiles => mbtiles}/tests/snapshots/mbtiles__convert@v1__z6__flat-hash.snap (96%) rename {martin-mbtiles => mbtiles}/tests/snapshots/mbtiles__convert@v1__z6__flat-norm.snap (98%) rename {martin-mbtiles => mbtiles}/tests/snapshots/mbtiles__convert@v1__z6__hash-flat.snap (95%) rename {martin-mbtiles => mbtiles}/tests/snapshots/mbtiles__convert@v1__z6__hash-hash.snap (96%) rename {martin-mbtiles => mbtiles}/tests/snapshots/mbtiles__convert@v1__z6__hash-norm.snap (98%) rename {martin-mbtiles => mbtiles}/tests/snapshots/mbtiles__convert@v1__z6__norm-flat.snap (95%) rename {martin-mbtiles => mbtiles}/tests/snapshots/mbtiles__convert@v1__z6__norm-hash.snap (96%) rename {martin-mbtiles => mbtiles}/tests/snapshots/mbtiles__convert@v1__z6__norm-norm.snap (98%) rename {martin-mbtiles => mbtiles}/tests/snapshots/mbtiles__databases@flat__dif.snap (96%) rename {martin-mbtiles => mbtiles}/tests/snapshots/mbtiles__databases@flat__v1-no-hash.snap (96%) rename {martin-mbtiles => mbtiles}/tests/snapshots/mbtiles__databases@flat__v1.snap (96%) rename {martin-mbtiles => mbtiles}/tests/snapshots/mbtiles__databases@flat__v2.snap (96%) rename {martin-mbtiles => mbtiles}/tests/snapshots/mbtiles__databases@hash__dif.snap (97%) rename {martin-mbtiles => mbtiles}/tests/snapshots/mbtiles__databases@hash__v1-no-hash.snap (97%) rename {martin-mbtiles => mbtiles}/tests/snapshots/mbtiles__databases@hash__v1.snap (97%) rename {martin-mbtiles => mbtiles}/tests/snapshots/mbtiles__databases@hash__v2.snap (97%) rename {martin-mbtiles => mbtiles}/tests/snapshots/mbtiles__databases@norm__dif.snap (98%) rename {martin-mbtiles => mbtiles}/tests/snapshots/mbtiles__databases@norm__v1-no-hash.snap (98%) rename {martin-mbtiles => mbtiles}/tests/snapshots/mbtiles__databases@norm__v1.snap (98%) rename {martin-mbtiles => mbtiles}/tests/snapshots/mbtiles__databases@norm__v2.snap (98%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 49a9c9871..3efd57928 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,8 +57,8 @@ jobs: if: github.event_name != 'release' && github.event_name != 'workflow_dispatch' - run: cargo fmt --all -- --check - run: cargo clippy --package martin-tile-utils -- -D warnings - - run: cargo clippy --package martin-mbtiles --no-default-features -- -D warnings - - run: cargo clippy --package martin-mbtiles -- -D warnings + - run: cargo clippy --package mbtiles --no-default-features -- -D warnings + - run: cargo clippy --package mbtiles -- -D warnings - run: cargo clippy --package martin -- -D warnings - run: cargo clippy --package martin --features bless-tests -- -D warnings - run: cargo doc --no-deps --workspace @@ -72,8 +72,8 @@ jobs: run: | set -x cargo test --package martin-tile-utils - cargo test --package martin-mbtiles --no-default-features - cargo test --package martin-mbtiles + cargo test --package mbtiles --no-default-features + cargo test --package mbtiles cargo test --package martin cargo test --doc env: @@ -144,7 +144,7 @@ jobs: echo "Building $target" export "CARGO_TARGET_$(echo $target | tr 'a-z-' 'A-Z_')_RUSTFLAGS"='-C strip=debuginfo' - cross build --release --target $target --package martin-mbtiles + cross build --release --target $target --package mbtiles cross build --release --target $target --package martin mkdir -p target_releases/$target @@ -274,7 +274,7 @@ jobs: set -x rustup target add "${{ matrix.target }}" export RUSTFLAGS='-C strip=debuginfo' - cargo build --release --target ${{ matrix.target }} --package martin-mbtiles + cargo build --release --target ${{ matrix.target }} --package mbtiles cargo build --release --target ${{ matrix.target }} --package martin mkdir -p target_releases mv target/${{ matrix.target }}/release/mbtiles${{ matrix.ext }} target_releases/ diff --git a/Cargo.lock b/Cargo.lock index 22b36fc1a..c485d5305 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -471,9 +471,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "2.5.0" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da74e2b81409b1b743f8f0c62cc6254afefb8b8e50bbfe3735550f7aeefa3448" +checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -1752,8 +1752,8 @@ dependencies = [ "itertools 0.11.0", "json-patch", "log", - "martin-mbtiles", "martin-tile-utils", + "mbtiles", "num_cpus", "pbf_font_tools", "pmtiles", @@ -1777,8 +1777,12 @@ dependencies = [ ] [[package]] -name = "martin-mbtiles" -version = "0.6.0" +name = "martin-tile-utils" +version = "0.1.3" + +[[package]] +name = "mbtiles" +version = "0.7.0" dependencies = [ "actix-rt", "anyhow", @@ -1802,10 +1806,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "martin-tile-utils" -version = "0.1.3" - [[package]] name = "md-5" version = "0.10.6" diff --git a/Cargo.toml b/Cargo.toml index 4c5718548..1bd28c932 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = ["martin", "martin-tile-utils", "martin-mbtiles"] +members = ["martin", "martin-tile-utils", "mbtiles"] [workspace.package] edition = "2021" @@ -33,8 +33,8 @@ insta = "1" itertools = "0.11" json-patch = "1.2" log = "0.4" -martin-mbtiles = { path = "./martin-mbtiles", version = "0.6.0", default-features = false } martin-tile-utils = { path = "./martin-tile-utils", version = "0.1.0" } +mbtiles = { path = "./mbtiles", version = "0.7.0", default-features = false } num_cpus = "1" pbf_font_tools = { version = "2.5.0", features = ["freetype"] } pmtiles = { version = "0.3", features = ["mmap-async-tokio", "tilejson"] } diff --git a/docs/src/50-tools.md b/docs/src/50-tools.md index 6d5c0dab6..e0d5ec06c 100644 --- a/docs/src/50-tools.md +++ b/docs/src/50-tools.md @@ -5,7 +5,7 @@ Martin has a few additional tools that can be used to interact with the data. ## MBTiles tool A small utility that allows users to interact with the `*.mbtiles` files from the command line. Use `mbtiles --help` to see a list of available commands, and `mbtiles --help` to see help for a specific command. -This tool can be installed by compiling the latest released version with `cargo install martin-mbtiles`, or by downloading a pre-built binary from the [releases page](https://github.com/maplibre/martin/releases/latest). +This tool can be installed by compiling the latest released version with `cargo install mbtiles`, or by downloading a pre-built binary from the [releases page](https://github.com/maplibre/martin/releases/latest). ### meta-all Print all metadata values to stdout, as well as the results of tile detection. The format of the values printed is not stable, and should only be used for visual inspection. diff --git a/justfile b/justfile index ece178f23..2f3862a3b 100644 --- a/justfile +++ b/justfile @@ -148,8 +148,8 @@ bless: restart clean-test bless-insta-martin bless-insta-mbtiles # Run integration tests and save its output as the new expected output bless-insta-mbtiles *ARGS: (cargo-install "cargo-insta") - #rm -rf martin-mbtiles/tests/snapshots - cargo insta test --accept --unreferenced=auto -p martin-mbtiles {{ ARGS }} + #rm -rf mbtiles/tests/snapshots + cargo insta test --accept --unreferenced=auto -p mbtiles {{ ARGS }} # Run integration tests and save its output as the new expected output bless-insta-martin *ARGS: (cargo-install "cargo-insta") @@ -256,8 +256,8 @@ git-pre-push: stop start # Update sqlite database schema. prepare-sqlite: install-sqlx - mkdir -p martin-mbtiles/.sqlx - cd martin-mbtiles && cargo sqlx prepare --database-url sqlite://$PWD/../tests/fixtures/mbtiles/world_cities.mbtiles -- --lib --tests + mkdir -p mbtiles/.sqlx + cd mbtiles && cargo sqlx prepare --database-url sqlite://$PWD/../tests/fixtures/mbtiles/world_cities.mbtiles -- --lib --tests # Install SQLX cli if not already installed. [private] diff --git a/martin/Cargo.toml b/martin/Cargo.toml index 08e7d06c2..cd9cfb791 100644 --- a/martin/Cargo.toml +++ b/martin/Cargo.toml @@ -66,8 +66,8 @@ futures.workspace = true itertools.workspace = true json-patch.workspace = true log.workspace = true -martin-mbtiles.workspace = true martin-tile-utils.workspace = true +mbtiles.workspace = true num_cpus.workspace = true pbf_font_tools.workspace = true pmtiles.workspace = true diff --git a/martin/src/mbtiles/mod.rs b/martin/src/mbtiles/mod.rs index 1190ffad1..ea4c84796 100644 --- a/martin/src/mbtiles/mod.rs +++ b/martin/src/mbtiles/mod.rs @@ -5,8 +5,8 @@ use std::sync::Arc; use async_trait::async_trait; use log::trace; -use martin_mbtiles::MbtilesPool; use martin_tile_utils::TileInfo; +use mbtiles::MbtilesPool; use tilejson::TileJSON; use crate::file_config::FileError; diff --git a/martin-mbtiles/.env b/mbtiles/.env similarity index 100% rename from martin-mbtiles/.env rename to mbtiles/.env diff --git a/martin-mbtiles/.sqlx/query-0a4540e8c33c71222a68ff5ecc1a167b406de9961ac3cc69649c6152a6d7a9b7.json b/mbtiles/.sqlx/query-0a4540e8c33c71222a68ff5ecc1a167b406de9961ac3cc69649c6152a6d7a9b7.json similarity index 100% rename from martin-mbtiles/.sqlx/query-0a4540e8c33c71222a68ff5ecc1a167b406de9961ac3cc69649c6152a6d7a9b7.json rename to mbtiles/.sqlx/query-0a4540e8c33c71222a68ff5ecc1a167b406de9961ac3cc69649c6152a6d7a9b7.json diff --git a/martin-mbtiles/.sqlx/query-176e99c6945b0789119d0d21a99de564de47dde1d588f17e68ec58115ac73a39.json b/mbtiles/.sqlx/query-176e99c6945b0789119d0d21a99de564de47dde1d588f17e68ec58115ac73a39.json similarity index 100% rename from martin-mbtiles/.sqlx/query-176e99c6945b0789119d0d21a99de564de47dde1d588f17e68ec58115ac73a39.json rename to mbtiles/.sqlx/query-176e99c6945b0789119d0d21a99de564de47dde1d588f17e68ec58115ac73a39.json diff --git a/martin-mbtiles/.sqlx/query-386a375cf65c3e5aef51deffc99d23bd852ba445c1058aed380fe83bed618c29.json b/mbtiles/.sqlx/query-386a375cf65c3e5aef51deffc99d23bd852ba445c1058aed380fe83bed618c29.json similarity index 100% rename from martin-mbtiles/.sqlx/query-386a375cf65c3e5aef51deffc99d23bd852ba445c1058aed380fe83bed618c29.json rename to mbtiles/.sqlx/query-386a375cf65c3e5aef51deffc99d23bd852ba445c1058aed380fe83bed618c29.json diff --git a/martin-mbtiles/.sqlx/query-428a035a55a07cbb9daac42c3ab05f2a7999788167f41c685af3ca6f5a1359f4.json b/mbtiles/.sqlx/query-428a035a55a07cbb9daac42c3ab05f2a7999788167f41c685af3ca6f5a1359f4.json similarity index 100% rename from martin-mbtiles/.sqlx/query-428a035a55a07cbb9daac42c3ab05f2a7999788167f41c685af3ca6f5a1359f4.json rename to mbtiles/.sqlx/query-428a035a55a07cbb9daac42c3ab05f2a7999788167f41c685af3ca6f5a1359f4.json diff --git a/martin-mbtiles/.sqlx/query-4d952966a8d8a030d2467c0701a6e16068c9897dd25d0ebd32929db9960596b4.json b/mbtiles/.sqlx/query-4d952966a8d8a030d2467c0701a6e16068c9897dd25d0ebd32929db9960596b4.json similarity index 100% rename from martin-mbtiles/.sqlx/query-4d952966a8d8a030d2467c0701a6e16068c9897dd25d0ebd32929db9960596b4.json rename to mbtiles/.sqlx/query-4d952966a8d8a030d2467c0701a6e16068c9897dd25d0ebd32929db9960596b4.json diff --git a/martin-mbtiles/.sqlx/query-5b298df51dccbf0d8a22433a99febc59c27dbf204d09a9c1fb0b3bf9aaad284b.json b/mbtiles/.sqlx/query-5b298df51dccbf0d8a22433a99febc59c27dbf204d09a9c1fb0b3bf9aaad284b.json similarity index 100% rename from martin-mbtiles/.sqlx/query-5b298df51dccbf0d8a22433a99febc59c27dbf204d09a9c1fb0b3bf9aaad284b.json rename to mbtiles/.sqlx/query-5b298df51dccbf0d8a22433a99febc59c27dbf204d09a9c1fb0b3bf9aaad284b.json diff --git a/martin-mbtiles/.sqlx/query-60264fa07915878b3f7ba0067f48c3a379e96acbdf5fc52d14e29bc726fefab7.json b/mbtiles/.sqlx/query-60264fa07915878b3f7ba0067f48c3a379e96acbdf5fc52d14e29bc726fefab7.json similarity index 100% rename from martin-mbtiles/.sqlx/query-60264fa07915878b3f7ba0067f48c3a379e96acbdf5fc52d14e29bc726fefab7.json rename to mbtiles/.sqlx/query-60264fa07915878b3f7ba0067f48c3a379e96acbdf5fc52d14e29bc726fefab7.json diff --git a/martin-mbtiles/.sqlx/query-7341bfc10beb4719811556a57ae8098085994c8fba93e0293359afd43079c50c.json b/mbtiles/.sqlx/query-7341bfc10beb4719811556a57ae8098085994c8fba93e0293359afd43079c50c.json similarity index 100% rename from martin-mbtiles/.sqlx/query-7341bfc10beb4719811556a57ae8098085994c8fba93e0293359afd43079c50c.json rename to mbtiles/.sqlx/query-7341bfc10beb4719811556a57ae8098085994c8fba93e0293359afd43079c50c.json diff --git a/martin-mbtiles/.sqlx/query-748436831449877b242d6e167a2f8fe1b1e7b6fb87c4e04ad7406a2bbfd35bec.json b/mbtiles/.sqlx/query-748436831449877b242d6e167a2f8fe1b1e7b6fb87c4e04ad7406a2bbfd35bec.json similarity index 100% rename from martin-mbtiles/.sqlx/query-748436831449877b242d6e167a2f8fe1b1e7b6fb87c4e04ad7406a2bbfd35bec.json rename to mbtiles/.sqlx/query-748436831449877b242d6e167a2f8fe1b1e7b6fb87c4e04ad7406a2bbfd35bec.json diff --git a/martin-mbtiles/.sqlx/query-77b2f46851c4e991230ec6a5d33aaca18373bbdd548a8378ae7fbeed351b4b87.json b/mbtiles/.sqlx/query-77b2f46851c4e991230ec6a5d33aaca18373bbdd548a8378ae7fbeed351b4b87.json similarity index 100% rename from martin-mbtiles/.sqlx/query-77b2f46851c4e991230ec6a5d33aaca18373bbdd548a8378ae7fbeed351b4b87.json rename to mbtiles/.sqlx/query-77b2f46851c4e991230ec6a5d33aaca18373bbdd548a8378ae7fbeed351b4b87.json diff --git a/martin-mbtiles/.sqlx/query-809e89c3b223e28c6716d405e13ba30fbf018805fe9ca2acd2b2e225183d1f13.json b/mbtiles/.sqlx/query-809e89c3b223e28c6716d405e13ba30fbf018805fe9ca2acd2b2e225183d1f13.json similarity index 100% rename from martin-mbtiles/.sqlx/query-809e89c3b223e28c6716d405e13ba30fbf018805fe9ca2acd2b2e225183d1f13.json rename to mbtiles/.sqlx/query-809e89c3b223e28c6716d405e13ba30fbf018805fe9ca2acd2b2e225183d1f13.json diff --git a/martin-mbtiles/.sqlx/query-85b46712c445679053e768cc98b22ea61633c21afb45d3d2b9aeec068d72cce0.json b/mbtiles/.sqlx/query-85b46712c445679053e768cc98b22ea61633c21afb45d3d2b9aeec068d72cce0.json similarity index 100% rename from martin-mbtiles/.sqlx/query-85b46712c445679053e768cc98b22ea61633c21afb45d3d2b9aeec068d72cce0.json rename to mbtiles/.sqlx/query-85b46712c445679053e768cc98b22ea61633c21afb45d3d2b9aeec068d72cce0.json diff --git a/martin-mbtiles/.sqlx/query-c8ef3dc53f1f6fd80e266aab2bf007c66a1cc45bdfcdc38f93d6ba759125a9aa.json b/mbtiles/.sqlx/query-c8ef3dc53f1f6fd80e266aab2bf007c66a1cc45bdfcdc38f93d6ba759125a9aa.json similarity index 100% rename from martin-mbtiles/.sqlx/query-c8ef3dc53f1f6fd80e266aab2bf007c66a1cc45bdfcdc38f93d6ba759125a9aa.json rename to mbtiles/.sqlx/query-c8ef3dc53f1f6fd80e266aab2bf007c66a1cc45bdfcdc38f93d6ba759125a9aa.json diff --git a/martin-mbtiles/.sqlx/query-d6ac76a234c97d0dc1fc4331d8b2cd90903d5401f8f0956245e5163bedd23a4d.json b/mbtiles/.sqlx/query-d6ac76a234c97d0dc1fc4331d8b2cd90903d5401f8f0956245e5163bedd23a4d.json similarity index 100% rename from martin-mbtiles/.sqlx/query-d6ac76a234c97d0dc1fc4331d8b2cd90903d5401f8f0956245e5163bedd23a4d.json rename to mbtiles/.sqlx/query-d6ac76a234c97d0dc1fc4331d8b2cd90903d5401f8f0956245e5163bedd23a4d.json diff --git a/martin-mbtiles/.sqlx/query-f547ff198e3bb604550a3f191e4ad8c695c4c2350f294aefd210eccec603d905.json b/mbtiles/.sqlx/query-f547ff198e3bb604550a3f191e4ad8c695c4c2350f294aefd210eccec603d905.json similarity index 100% rename from martin-mbtiles/.sqlx/query-f547ff198e3bb604550a3f191e4ad8c695c4c2350f294aefd210eccec603d905.json rename to mbtiles/.sqlx/query-f547ff198e3bb604550a3f191e4ad8c695c4c2350f294aefd210eccec603d905.json diff --git a/martin-mbtiles/Cargo.toml b/mbtiles/Cargo.toml similarity index 97% rename from martin-mbtiles/Cargo.toml rename to mbtiles/Cargo.toml index ef100d7fa..0028c4797 100644 --- a/martin-mbtiles/Cargo.toml +++ b/mbtiles/Cargo.toml @@ -1,6 +1,6 @@ [package] -name = "martin-mbtiles" -version = "0.6.0" +name = "mbtiles" +version = "0.7.0" authors = ["Yuri Astrakhan ", "MapLibre contributors"] description = "A simple low-level MbTiles access and processing library, with some tile format detection and other relevant heuristics." keywords = ["mbtiles", "maps", "tiles", "mvt", "tilejson"] diff --git a/martin-mbtiles/README.md b/mbtiles/README.md similarity index 84% rename from martin-mbtiles/README.md rename to mbtiles/README.md index 2963f4076..cc956ebc8 100644 --- a/martin-mbtiles/README.md +++ b/mbtiles/README.md @@ -1,11 +1,11 @@ -# martin-mbtiles +# mbtiles [![Book](https://img.shields.io/badge/docs-Book-informational)](https://maplibre.org/martin/50-tools.html) -[![docs.rs docs](https://docs.rs/martin-mbtiles/badge.svg)](https://docs.rs/martin-mbtiles) +[![docs.rs docs](https://docs.rs/mbtiles/badge.svg)](https://docs.rs/mbtiles) [![Slack chat](https://img.shields.io/badge/Chat-on%20Slack-blueviolet)](https://slack.openstreetmap.us/) [![GitHub](https://img.shields.io/badge/github-maplibre/martin-8da0cb?logo=github)](https://github.com/maplibre/martin) -[![crates.io version](https://img.shields.io/crates/v/martin-mbtiles.svg)](https://crates.io/crates/martin-mbtiles) -[![CI build](https://github.com/maplibre/martin/workflows/CI/badge.svg)](https://github.com/maplibre/martin-mbtiles/actions) +[![crates.io version](https://img.shields.io/crates/v/mbtiles.svg)](https://crates.io/crates/mbtiles) +[![CI build](https://github.com/maplibre/martin/workflows/CI/badge.svg)](https://github.com/maplibre/martin/actions) A library to help tile servers like [Martin](https://maplibre.org/martin) work with [MBTiles](https://github.com/mapbox/mbtiles-spec) files. When using as a lib, you may want to disable default features (i.e. the unused "cli" feature). diff --git a/martin-mbtiles/src/bin/main.rs b/mbtiles/src/bin/main.rs similarity index 98% rename from martin-mbtiles/src/bin/main.rs rename to mbtiles/src/bin/main.rs index 8842a64b3..9643e5f54 100644 --- a/martin-mbtiles/src/bin/main.rs +++ b/mbtiles/src/bin/main.rs @@ -2,7 +2,7 @@ use std::path::{Path, PathBuf}; use clap::{Parser, Subcommand}; use log::error; -use martin_mbtiles::{apply_patch, IntegrityCheckType, MbtResult, Mbtiles, MbtilesCopier}; +use mbtiles::{apply_patch, IntegrityCheckType, MbtResult, Mbtiles, MbtilesCopier}; #[derive(Parser, PartialEq, Eq, Debug)] #[command( @@ -148,7 +148,7 @@ mod tests { use clap::error::ErrorKind; use clap::Parser; - use martin_mbtiles::{CopyDuplicateMode, MbtilesCopier}; + use mbtiles::{CopyDuplicateMode, MbtilesCopier}; use crate::Commands::{ApplyPatch, Copy, MetaGetValue, MetaSetValue, Validate}; use crate::{Args, IntegrityCheckType}; diff --git a/martin-mbtiles/src/copier.rs b/mbtiles/src/copier.rs similarity index 100% rename from martin-mbtiles/src/copier.rs rename to mbtiles/src/copier.rs diff --git a/martin-mbtiles/src/errors.rs b/mbtiles/src/errors.rs similarity index 100% rename from martin-mbtiles/src/errors.rs rename to mbtiles/src/errors.rs diff --git a/martin-mbtiles/src/lib.rs b/mbtiles/src/lib.rs similarity index 100% rename from martin-mbtiles/src/lib.rs rename to mbtiles/src/lib.rs diff --git a/martin-mbtiles/src/mbtiles.rs b/mbtiles/src/mbtiles.rs similarity index 100% rename from martin-mbtiles/src/mbtiles.rs rename to mbtiles/src/mbtiles.rs diff --git a/martin-mbtiles/src/patcher.rs b/mbtiles/src/patcher.rs similarity index 100% rename from martin-mbtiles/src/patcher.rs rename to mbtiles/src/patcher.rs diff --git a/martin-mbtiles/src/pool.rs b/mbtiles/src/pool.rs similarity index 100% rename from martin-mbtiles/src/pool.rs rename to mbtiles/src/pool.rs diff --git a/martin-mbtiles/src/queries.rs b/mbtiles/src/queries.rs similarity index 100% rename from martin-mbtiles/src/queries.rs rename to mbtiles/src/queries.rs diff --git a/martin-mbtiles/tests/mbtiles.rs b/mbtiles/tests/mbtiles.rs similarity index 98% rename from martin-mbtiles/tests/mbtiles.rs rename to mbtiles/tests/mbtiles.rs index 2e7ec641b..8782cda8f 100644 --- a/martin-mbtiles/tests/mbtiles.rs +++ b/mbtiles/tests/mbtiles.rs @@ -5,11 +5,9 @@ use std::str::from_utf8; use ctor::ctor; use insta::{allow_duplicates, assert_display_snapshot}; use log::info; -use martin_mbtiles::IntegrityCheckType::Off; -use martin_mbtiles::MbtTypeCli::{Flat, FlatWithHash, Normalized}; -use martin_mbtiles::{ - apply_patch, create_flat_tables, MbtResult, MbtTypeCli, Mbtiles, MbtilesCopier, -}; +use mbtiles::IntegrityCheckType::Off; +use mbtiles::MbtTypeCli::{Flat, FlatWithHash, Normalized}; +use mbtiles::{apply_patch, create_flat_tables, MbtResult, MbtTypeCli, Mbtiles, MbtilesCopier}; use pretty_assertions::assert_eq as pretty_assert_eq; use rstest::{fixture, rstest}; use serde::Serialize; diff --git a/martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__flat-flat.snap b/mbtiles/tests/snapshots/mbtiles__convert@v1__z6__flat-flat.snap similarity index 95% rename from martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__flat-flat.snap rename to mbtiles/tests/snapshots/mbtiles__convert@v1__z6__flat-flat.snap index f0a89da1d..4570d4c73 100644 --- a/martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__flat-flat.snap +++ b/mbtiles/tests/snapshots/mbtiles__convert@v1__z6__flat-flat.snap @@ -1,5 +1,5 @@ --- -source: martin-mbtiles/tests/mbtiles.rs +source: mbtiles/tests/mbtiles.rs expression: actual_value --- [[]] diff --git a/martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__flat-hash.snap b/mbtiles/tests/snapshots/mbtiles__convert@v1__z6__flat-hash.snap similarity index 96% rename from martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__flat-hash.snap rename to mbtiles/tests/snapshots/mbtiles__convert@v1__z6__flat-hash.snap index 19aa43b08..76f3e7040 100644 --- a/martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__flat-hash.snap +++ b/mbtiles/tests/snapshots/mbtiles__convert@v1__z6__flat-hash.snap @@ -1,5 +1,5 @@ --- -source: martin-mbtiles/tests/mbtiles.rs +source: mbtiles/tests/mbtiles.rs expression: actual_value --- [[]] diff --git a/martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__flat-norm.snap b/mbtiles/tests/snapshots/mbtiles__convert@v1__z6__flat-norm.snap similarity index 98% rename from martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__flat-norm.snap rename to mbtiles/tests/snapshots/mbtiles__convert@v1__z6__flat-norm.snap index d129463dd..bce869b3b 100644 --- a/martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__flat-norm.snap +++ b/mbtiles/tests/snapshots/mbtiles__convert@v1__z6__flat-norm.snap @@ -1,5 +1,5 @@ --- -source: martin-mbtiles/tests/mbtiles.rs +source: mbtiles/tests/mbtiles.rs expression: actual_value --- [[]] diff --git a/martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__hash-flat.snap b/mbtiles/tests/snapshots/mbtiles__convert@v1__z6__hash-flat.snap similarity index 95% rename from martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__hash-flat.snap rename to mbtiles/tests/snapshots/mbtiles__convert@v1__z6__hash-flat.snap index f0a89da1d..4570d4c73 100644 --- a/martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__hash-flat.snap +++ b/mbtiles/tests/snapshots/mbtiles__convert@v1__z6__hash-flat.snap @@ -1,5 +1,5 @@ --- -source: martin-mbtiles/tests/mbtiles.rs +source: mbtiles/tests/mbtiles.rs expression: actual_value --- [[]] diff --git a/martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__hash-hash.snap b/mbtiles/tests/snapshots/mbtiles__convert@v1__z6__hash-hash.snap similarity index 96% rename from martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__hash-hash.snap rename to mbtiles/tests/snapshots/mbtiles__convert@v1__z6__hash-hash.snap index 19aa43b08..76f3e7040 100644 --- a/martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__hash-hash.snap +++ b/mbtiles/tests/snapshots/mbtiles__convert@v1__z6__hash-hash.snap @@ -1,5 +1,5 @@ --- -source: martin-mbtiles/tests/mbtiles.rs +source: mbtiles/tests/mbtiles.rs expression: actual_value --- [[]] diff --git a/martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__hash-norm.snap b/mbtiles/tests/snapshots/mbtiles__convert@v1__z6__hash-norm.snap similarity index 98% rename from martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__hash-norm.snap rename to mbtiles/tests/snapshots/mbtiles__convert@v1__z6__hash-norm.snap index d129463dd..bce869b3b 100644 --- a/martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__hash-norm.snap +++ b/mbtiles/tests/snapshots/mbtiles__convert@v1__z6__hash-norm.snap @@ -1,5 +1,5 @@ --- -source: martin-mbtiles/tests/mbtiles.rs +source: mbtiles/tests/mbtiles.rs expression: actual_value --- [[]] diff --git a/martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__norm-flat.snap b/mbtiles/tests/snapshots/mbtiles__convert@v1__z6__norm-flat.snap similarity index 95% rename from martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__norm-flat.snap rename to mbtiles/tests/snapshots/mbtiles__convert@v1__z6__norm-flat.snap index f0a89da1d..4570d4c73 100644 --- a/martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__norm-flat.snap +++ b/mbtiles/tests/snapshots/mbtiles__convert@v1__z6__norm-flat.snap @@ -1,5 +1,5 @@ --- -source: martin-mbtiles/tests/mbtiles.rs +source: mbtiles/tests/mbtiles.rs expression: actual_value --- [[]] diff --git a/martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__norm-hash.snap b/mbtiles/tests/snapshots/mbtiles__convert@v1__z6__norm-hash.snap similarity index 96% rename from martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__norm-hash.snap rename to mbtiles/tests/snapshots/mbtiles__convert@v1__z6__norm-hash.snap index 19aa43b08..76f3e7040 100644 --- a/martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__norm-hash.snap +++ b/mbtiles/tests/snapshots/mbtiles__convert@v1__z6__norm-hash.snap @@ -1,5 +1,5 @@ --- -source: martin-mbtiles/tests/mbtiles.rs +source: mbtiles/tests/mbtiles.rs expression: actual_value --- [[]] diff --git a/martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__norm-norm.snap b/mbtiles/tests/snapshots/mbtiles__convert@v1__z6__norm-norm.snap similarity index 98% rename from martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__norm-norm.snap rename to mbtiles/tests/snapshots/mbtiles__convert@v1__z6__norm-norm.snap index d129463dd..bce869b3b 100644 --- a/martin-mbtiles/tests/snapshots/mbtiles__convert@v1__z6__norm-norm.snap +++ b/mbtiles/tests/snapshots/mbtiles__convert@v1__z6__norm-norm.snap @@ -1,5 +1,5 @@ --- -source: martin-mbtiles/tests/mbtiles.rs +source: mbtiles/tests/mbtiles.rs expression: actual_value --- [[]] diff --git a/martin-mbtiles/tests/snapshots/mbtiles__databases@flat__dif.snap b/mbtiles/tests/snapshots/mbtiles__databases@flat__dif.snap similarity index 96% rename from martin-mbtiles/tests/snapshots/mbtiles__databases@flat__dif.snap rename to mbtiles/tests/snapshots/mbtiles__databases@flat__dif.snap index 84ccfd9e1..d5b5003ec 100644 --- a/martin-mbtiles/tests/snapshots/mbtiles__databases@flat__dif.snap +++ b/mbtiles/tests/snapshots/mbtiles__databases@flat__dif.snap @@ -1,5 +1,5 @@ --- -source: martin-mbtiles/tests/mbtiles.rs +source: mbtiles/tests/mbtiles.rs expression: actual_value --- [[]] diff --git a/martin-mbtiles/tests/snapshots/mbtiles__databases@flat__v1-no-hash.snap b/mbtiles/tests/snapshots/mbtiles__databases@flat__v1-no-hash.snap similarity index 96% rename from martin-mbtiles/tests/snapshots/mbtiles__databases@flat__v1-no-hash.snap rename to mbtiles/tests/snapshots/mbtiles__databases@flat__v1-no-hash.snap index 613d2e8ef..d2d27f6d4 100644 --- a/martin-mbtiles/tests/snapshots/mbtiles__databases@flat__v1-no-hash.snap +++ b/mbtiles/tests/snapshots/mbtiles__databases@flat__v1-no-hash.snap @@ -1,5 +1,5 @@ --- -source: martin-mbtiles/tests/mbtiles.rs +source: mbtiles/tests/mbtiles.rs expression: actual_value --- [[]] diff --git a/martin-mbtiles/tests/snapshots/mbtiles__databases@flat__v1.snap b/mbtiles/tests/snapshots/mbtiles__databases@flat__v1.snap similarity index 96% rename from martin-mbtiles/tests/snapshots/mbtiles__databases@flat__v1.snap rename to mbtiles/tests/snapshots/mbtiles__databases@flat__v1.snap index cccaf7fb0..9f40369d0 100644 --- a/martin-mbtiles/tests/snapshots/mbtiles__databases@flat__v1.snap +++ b/mbtiles/tests/snapshots/mbtiles__databases@flat__v1.snap @@ -1,5 +1,5 @@ --- -source: martin-mbtiles/tests/mbtiles.rs +source: mbtiles/tests/mbtiles.rs expression: actual_value --- [[]] diff --git a/martin-mbtiles/tests/snapshots/mbtiles__databases@flat__v2.snap b/mbtiles/tests/snapshots/mbtiles__databases@flat__v2.snap similarity index 96% rename from martin-mbtiles/tests/snapshots/mbtiles__databases@flat__v2.snap rename to mbtiles/tests/snapshots/mbtiles__databases@flat__v2.snap index c3b44ccb6..af065171f 100644 --- a/martin-mbtiles/tests/snapshots/mbtiles__databases@flat__v2.snap +++ b/mbtiles/tests/snapshots/mbtiles__databases@flat__v2.snap @@ -1,5 +1,5 @@ --- -source: martin-mbtiles/tests/mbtiles.rs +source: mbtiles/tests/mbtiles.rs expression: actual_value --- [[]] diff --git a/martin-mbtiles/tests/snapshots/mbtiles__databases@hash__dif.snap b/mbtiles/tests/snapshots/mbtiles__databases@hash__dif.snap similarity index 97% rename from martin-mbtiles/tests/snapshots/mbtiles__databases@hash__dif.snap rename to mbtiles/tests/snapshots/mbtiles__databases@hash__dif.snap index 7d01a84f9..7584b75db 100644 --- a/martin-mbtiles/tests/snapshots/mbtiles__databases@hash__dif.snap +++ b/mbtiles/tests/snapshots/mbtiles__databases@hash__dif.snap @@ -1,5 +1,5 @@ --- -source: martin-mbtiles/tests/mbtiles.rs +source: mbtiles/tests/mbtiles.rs expression: actual_value --- [[]] diff --git a/martin-mbtiles/tests/snapshots/mbtiles__databases@hash__v1-no-hash.snap b/mbtiles/tests/snapshots/mbtiles__databases@hash__v1-no-hash.snap similarity index 97% rename from martin-mbtiles/tests/snapshots/mbtiles__databases@hash__v1-no-hash.snap rename to mbtiles/tests/snapshots/mbtiles__databases@hash__v1-no-hash.snap index db7f84fd9..163c7566e 100644 --- a/martin-mbtiles/tests/snapshots/mbtiles__databases@hash__v1-no-hash.snap +++ b/mbtiles/tests/snapshots/mbtiles__databases@hash__v1-no-hash.snap @@ -1,5 +1,5 @@ --- -source: martin-mbtiles/tests/mbtiles.rs +source: mbtiles/tests/mbtiles.rs expression: actual_value --- [[]] diff --git a/martin-mbtiles/tests/snapshots/mbtiles__databases@hash__v1.snap b/mbtiles/tests/snapshots/mbtiles__databases@hash__v1.snap similarity index 97% rename from martin-mbtiles/tests/snapshots/mbtiles__databases@hash__v1.snap rename to mbtiles/tests/snapshots/mbtiles__databases@hash__v1.snap index 0668dab3e..a9c1e4ea1 100644 --- a/martin-mbtiles/tests/snapshots/mbtiles__databases@hash__v1.snap +++ b/mbtiles/tests/snapshots/mbtiles__databases@hash__v1.snap @@ -1,5 +1,5 @@ --- -source: martin-mbtiles/tests/mbtiles.rs +source: mbtiles/tests/mbtiles.rs expression: actual_value --- [[]] diff --git a/martin-mbtiles/tests/snapshots/mbtiles__databases@hash__v2.snap b/mbtiles/tests/snapshots/mbtiles__databases@hash__v2.snap similarity index 97% rename from martin-mbtiles/tests/snapshots/mbtiles__databases@hash__v2.snap rename to mbtiles/tests/snapshots/mbtiles__databases@hash__v2.snap index d4cedb545..abfa9ceff 100644 --- a/martin-mbtiles/tests/snapshots/mbtiles__databases@hash__v2.snap +++ b/mbtiles/tests/snapshots/mbtiles__databases@hash__v2.snap @@ -1,5 +1,5 @@ --- -source: martin-mbtiles/tests/mbtiles.rs +source: mbtiles/tests/mbtiles.rs expression: actual_value --- [[]] diff --git a/martin-mbtiles/tests/snapshots/mbtiles__databases@norm__dif.snap b/mbtiles/tests/snapshots/mbtiles__databases@norm__dif.snap similarity index 98% rename from martin-mbtiles/tests/snapshots/mbtiles__databases@norm__dif.snap rename to mbtiles/tests/snapshots/mbtiles__databases@norm__dif.snap index cad93944a..95cf04a3b 100644 --- a/martin-mbtiles/tests/snapshots/mbtiles__databases@norm__dif.snap +++ b/mbtiles/tests/snapshots/mbtiles__databases@norm__dif.snap @@ -1,5 +1,5 @@ --- -source: martin-mbtiles/tests/mbtiles.rs +source: mbtiles/tests/mbtiles.rs expression: actual_value --- [[]] diff --git a/martin-mbtiles/tests/snapshots/mbtiles__databases@norm__v1-no-hash.snap b/mbtiles/tests/snapshots/mbtiles__databases@norm__v1-no-hash.snap similarity index 98% rename from martin-mbtiles/tests/snapshots/mbtiles__databases@norm__v1-no-hash.snap rename to mbtiles/tests/snapshots/mbtiles__databases@norm__v1-no-hash.snap index 62067b4a5..79da9dcff 100644 --- a/martin-mbtiles/tests/snapshots/mbtiles__databases@norm__v1-no-hash.snap +++ b/mbtiles/tests/snapshots/mbtiles__databases@norm__v1-no-hash.snap @@ -1,5 +1,5 @@ --- -source: martin-mbtiles/tests/mbtiles.rs +source: mbtiles/tests/mbtiles.rs expression: actual_value --- [[]] diff --git a/martin-mbtiles/tests/snapshots/mbtiles__databases@norm__v1.snap b/mbtiles/tests/snapshots/mbtiles__databases@norm__v1.snap similarity index 98% rename from martin-mbtiles/tests/snapshots/mbtiles__databases@norm__v1.snap rename to mbtiles/tests/snapshots/mbtiles__databases@norm__v1.snap index 5ee4cf9e4..32d7e7078 100644 --- a/martin-mbtiles/tests/snapshots/mbtiles__databases@norm__v1.snap +++ b/mbtiles/tests/snapshots/mbtiles__databases@norm__v1.snap @@ -1,5 +1,5 @@ --- -source: martin-mbtiles/tests/mbtiles.rs +source: mbtiles/tests/mbtiles.rs expression: actual_value --- [[]] diff --git a/martin-mbtiles/tests/snapshots/mbtiles__databases@norm__v2.snap b/mbtiles/tests/snapshots/mbtiles__databases@norm__v2.snap similarity index 98% rename from martin-mbtiles/tests/snapshots/mbtiles__databases@norm__v2.snap rename to mbtiles/tests/snapshots/mbtiles__databases@norm__v2.snap index 2bd5bbeb5..e7d9aac70 100644 --- a/martin-mbtiles/tests/snapshots/mbtiles__databases@norm__v2.snap +++ b/mbtiles/tests/snapshots/mbtiles__databases@norm__v2.snap @@ -1,5 +1,5 @@ --- -source: martin-mbtiles/tests/mbtiles.rs +source: mbtiles/tests/mbtiles.rs expression: actual_value --- [[]] diff --git a/tests/test.sh b/tests/test.sh index ec3dbb2dc..2ea687448 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -10,7 +10,7 @@ MARTIN_URL="http://localhost:${MARTIN_PORT}" MARTIN_ARGS="${MARTIN_ARGS:---listen-addresses localhost:${MARTIN_PORT}}" MARTIN_BIN="${MARTIN_BIN:-cargo run --} ${MARTIN_ARGS}" -MBTILES_BUILD="${MBTILES_BUILD:-cargo build -p martin-mbtiles}" +MBTILES_BUILD="${MBTILES_BUILD:-cargo build -p mbtiles}" MBTILES_BIN="${MBTILES_BIN:-target/debug/mbtiles}" LOG_DIR="${LOG_DIR:-target/test_logs}" From f1241e264c16486f98fbeef44f11f26bc81964c5 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Sat, 28 Oct 2023 16:29:30 -0400 Subject: [PATCH 089/108] fix CI status badges --- Cargo.lock | 2 +- README.md | 2 +- docs/src/00-introduction.md | 2 +- martin-tile-utils/README.md | 2 +- mbtiles/Cargo.toml | 2 +- mbtiles/README.md | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c485d5305..b617b0fae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1782,7 +1782,7 @@ version = "0.1.3" [[package]] name = "mbtiles" -version = "0.7.0" +version = "0.7.1" dependencies = [ "actix-rt", "anyhow", diff --git a/README.md b/README.md index eb7f07b43..4cc59382a 100755 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![GitHub](https://img.shields.io/badge/github-maplibre/martin-8da0cb?logo=github)](https://github.com/maplibre/martin) [![crates.io version](https://img.shields.io/crates/v/martin.svg)](https://crates.io/crates/martin) [![Security audit](https://github.com/maplibre/martin/workflows/Security%20audit/badge.svg)](https://github.com/maplibre/martin/security) -[![CI build](https://github.com/maplibre/martin/workflows/CI/badge.svg)](https://github.com/maplibre/martin/actions) +[![CI build](https://github.com/maplibre/martin/actions/workflows/ci.yml/badge.svg)](https://github.com/maplibre/martin/actions) [![](https://img.shields.io/badge/Slack-%23maplibre--martin-2EB67D?logo=slack)](https://slack.openstreetmap.us/) Martin is a tile server able to generate and serve [vector tiles](https://github.com/mapbox/vector-tile-spec) on the fly from large [PostGIS](https://github.com/postgis/postgis) databases, [PMTile](https://protomaps.com/blog/pmtiles-v3-whats-new), and [MBTile](https://github.com/mapbox/mbtiles-spec) files, allowing multiple tile sources to be dynamically combined into one. Martin optimizes for speed and heavy traffic, and is written in [Rust](https://github.com/rust-lang/rust). diff --git a/docs/src/00-introduction.md b/docs/src/00-introduction.md index c9c0b95e5..975889105 100644 --- a/docs/src/00-introduction.md +++ b/docs/src/00-introduction.md @@ -12,4 +12,4 @@ See also [Martin demo site](https://martin.maplibre.org/) [![GitHub](https://img.shields.io/badge/github-maplibre/martin-8da0cb?logo=github)](https://github.com/maplibre/martin) [![crates.io version](https://img.shields.io/crates/v/martin.svg)](https://crates.io/crates/martin) [![Security audit](https://github.com/maplibre/martin/workflows/Security%20audit/badge.svg)](https://github.com/maplibre/martin/security) -[![CI build](https://github.com/maplibre/martin/workflows/CI/badge.svg)](https://github.com/maplibre/martin/actions) +[![CI build](https://github.com/maplibre/martin/actions/workflows/ci.yml/badge.svg)](https://github.com/maplibre/martin/actions) diff --git a/martin-tile-utils/README.md b/martin-tile-utils/README.md index 2bdea051d..ab5520971 100644 --- a/martin-tile-utils/README.md +++ b/martin-tile-utils/README.md @@ -4,7 +4,7 @@ [![Slack chat](https://img.shields.io/badge/Chat-on%20Slack-blueviolet)](https://slack.openstreetmap.us/) [![GitHub](https://img.shields.io/badge/github-maplibre/martin-8da0cb?logo=github)](https://github.com/maplibre/martin) [![crates.io version](https://img.shields.io/crates/v/martin-tile-utils.svg)](https://crates.io/crates/martin-tile-utils) -[![CI build](https://github.com/maplibre/martin/workflows/CI/badge.svg)](https://github.com/maplibre/martin-tile-utils/actions) +[![CI build](https://github.com/maplibre/martin/actions/workflows/ci.yml/badge.svg)](https://github.com/maplibre/martin-tile-utils/actions) A library to help tile servers like [Martin](https://maplibre.org/martin) work with tile content. diff --git a/mbtiles/Cargo.toml b/mbtiles/Cargo.toml index 0028c4797..fc1051be1 100644 --- a/mbtiles/Cargo.toml +++ b/mbtiles/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mbtiles" -version = "0.7.0" +version = "0.7.1" authors = ["Yuri Astrakhan ", "MapLibre contributors"] description = "A simple low-level MbTiles access and processing library, with some tile format detection and other relevant heuristics." keywords = ["mbtiles", "maps", "tiles", "mvt", "tilejson"] diff --git a/mbtiles/README.md b/mbtiles/README.md index cc956ebc8..92e29a62a 100644 --- a/mbtiles/README.md +++ b/mbtiles/README.md @@ -5,7 +5,7 @@ [![Slack chat](https://img.shields.io/badge/Chat-on%20Slack-blueviolet)](https://slack.openstreetmap.us/) [![GitHub](https://img.shields.io/badge/github-maplibre/martin-8da0cb?logo=github)](https://github.com/maplibre/martin) [![crates.io version](https://img.shields.io/crates/v/mbtiles.svg)](https://crates.io/crates/mbtiles) -[![CI build](https://github.com/maplibre/martin/workflows/CI/badge.svg)](https://github.com/maplibre/martin/actions) +[![CI build](https://github.com/maplibre/martin/actions/workflows/ci.yml/badge.svg)](https://github.com/maplibre/martin/actions) A library to help tile servers like [Martin](https://maplibre.org/martin) work with [MBTiles](https://github.com/mapbox/mbtiles-spec) files. When using as a lib, you may want to disable default features (i.e. the unused "cli" feature). From 8c750aa3ccd5585db4fff1607c6465bc411e731b Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Mon, 30 Oct 2023 14:53:11 -0400 Subject: [PATCH 090/108] bump lock --- Cargo.lock | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b617b0fae..a1be09bff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -505,9 +505,9 @@ checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "bytestring" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "238e4886760d98c4f899360c834fa93e62cf7f721ac3c2da375cbdf4b8679aae" +checksum = "74d80203ea6b29df88012294f62733de21cfeab47f17b41af3a38bc30a03ee72" dependencies = [ "bytes", ] @@ -1697,9 +1697,9 @@ checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" [[package]] name = "local-channel" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a493488de5f18c8ffcba89eebb8532ffc562dc400490eb65b84893fae0b178" +checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" dependencies = [ "futures-core", "futures-sink", @@ -1708,9 +1708,9 @@ dependencies = [ [[package]] name = "local-waker" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e34f76eb3611940e0e7d53a9aaa4e6a3151f69541a282fd0dad5571420c53ff1" +checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" [[package]] name = "lock_api" @@ -2568,9 +2568,9 @@ dependencies = [ [[package]] name = "rgb" -version = "0.8.36" +version = "0.8.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20ec2d3e3fc7a92ced357df9cebd5a10b6fb2aa1ee797bf7e9ce2f17dffc8f59" +checksum = "05aaa8004b64fd573fc9d002f4e632d51ad4f026c2b5ba95fcb6c2f32c2c47d8" dependencies = [ "bytemuck", ] @@ -2870,9 +2870,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.107" +version = "1.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" dependencies = [ "indexmap 2.0.2", "itoa", @@ -4200,18 +4200,18 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" [[package]] name = "zerocopy" -version = "0.7.18" +version = "0.7.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7d7c7970ca2215b8c1ccf4d4f354c4733201dfaaba72d44ae5b37472e4901" +checksum = "dd66a62464e3ffd4e37bd09950c2b9dd6c4f8767380fabba0d523f9a775bc85a" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.18" +version = "0.7.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b27b1bb92570f989aac0ab7e9cbfbacdd65973f7ee920d9f0e71ebac878fd0b" +checksum = "255c4596d41e6916ced49cfafea18727b24d67878fa180ddfd69b9df34fd1726" dependencies = [ "proc-macro2", "quote", From c02fa4a6686c2208fc18516fdb2f468361e5bb77 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Mon, 30 Oct 2023 17:18:03 -0400 Subject: [PATCH 091/108] Fix CI workflow --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3efd57928..f717e20dc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -628,8 +628,7 @@ jobs: path: target/homebrew - name: Create Homebrew formula - if: startsWith(github.ref, 'refs/tags/') - uses: cuchi/jinja2-action@v1.2.1 + uses: ajeffowens/jinja2-action@90dab3da2215932ea86d2875224f06bbd6798617 # v2.0.0 with: template: .github/templates/homebrew.martin.rb.j2 output_file: target/homebrew/martin.rb From 163b44982a699ed4db8a82778b18e87acae5ab99 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Mon, 30 Oct 2023 17:45:31 -0400 Subject: [PATCH 092/108] fix CI --- .github/workflows/ci.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f717e20dc..ad706b479 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -584,7 +584,6 @@ jobs: cd .. - name: Create Homebrew config - if: startsWith(github.ref, 'refs/tags/v') run: | set -x @@ -603,7 +602,6 @@ jobs: EOF - name: Save Homebrew Config - if: startsWith(github.ref, 'refs/tags/') uses: actions/upload-artifact@v3 with: name: homebrew-config From 127c989f37001b2fe7790ac9d382387f6255393c Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Mon, 30 Oct 2023 18:18:03 -0400 Subject: [PATCH 093/108] fix font docs --- README.md | 24 +++++++++++--------- docs/src/40-using-endpoints.md | 41 +++++++++++++++++++++++++--------- 2 files changed, 44 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 4cc59382a..99461909e 100755 --- a/README.md +++ b/README.md @@ -86,20 +86,22 @@ docker run -p 3000:3000 \ ``` ## API -_See [API documentation](https://maplibre.org/martin/40-using.html) in the Martin book._ +_See [API documentation](https://maplibre.org/martin/40-using-endpoints.html) in the Martin book._ Martin data is available via the HTTP `GET` endpoints: -| URL | Description | -|----------------------------------------|-----------------------------------------------| -| `/` | Status text, that will eventually show web UI | -| `/catalog` | List of all sources | -| `/{sourceID}` | Source TileJSON | -| `/{sourceID}/{z}/{x}/{y}` | Map Tiles | -| `/{source1},...,{sourceN}` | Composite Source TileJSON | -| `/{source1},...,{sourceN}/{z}/{x}/{y}` | Composite Source Tiles | -| `/sprite/{spriteID}[@2x].{json,png}` | Sprites (low and high DPI, index/png) | -| `/health` | Martin server health check: returns 200 `OK` | +| URL | Description | +|-----------------------------------------|-----------------------------------------------| +| `/` | Status text, that will eventually show web UI | +| `/catalog` | List of all sources | +| `/{sourceID}` | Source TileJSON | +| `/{sourceID}/{z}/{x}/{y}` | Map Tiles | +| `/{source1},…,{sourceN}` | Composite Source TileJSON | +| `/{source1},…,{sourceN}/{z}/{x}/{y}` | Composite Source Tiles | +| `/sprite/{spriteID}[@2x].{json,png}` | Sprites (low and high DPI, index/png) | +| `/font/{font}/{start}-{end}` | Font source | +| `/font/{font1},…,{fontN}/{start}-{end}` | Composite Font source | +| `/health` | Martin server health check: returns 200 `OK` | ## Documentation See [Martin book](https://maplibre.org/martin/) for complete documentation. diff --git a/docs/src/40-using-endpoints.md b/docs/src/40-using-endpoints.md index 5130845aa..201750ec0 100644 --- a/docs/src/40-using-endpoints.md +++ b/docs/src/40-using-endpoints.md @@ -2,16 +2,18 @@ Martin data is available via the HTTP `GET` endpoints: -| URL | Description | -|----------------------------------------|------------------------------------------------| -| `/` | Status text, that will eventually show web UI | -| `/catalog` | [List of all sources](#catalog) | -| `/{sourceID}` | [Source TileJSON](#source-tilejson) | -| `/{sourceID}/{z}/{x}/{y}` | Map Tiles | -| `/{source1},...,{sourceN}` | [Composite Source TileJSON](#source-tilejson) | -| `/{source1},...,{sourceN}/{z}/{x}/{y}` | [Composite Source Tiles](30-config-file.md) | -| `/sprite/{spriteID}[@2x].{json,png}` | [Sprite sources](36-sources-spritess.md) | -| `/health` | Martin server health check: returns 200 `OK` | +| URL | Description | +|-----------------------------------------|-----------------------------------------------| +| `/` | Status text, that will eventually show web UI | +| `/catalog` | [List of all sources](#catalog) | +| `/{sourceID}` | [Source TileJSON](#source-tilejson) | +| `/{sourceID}/{z}/{x}/{y}` | Map Tiles | +| `/{source1},…,{sourceN}` | [Composite Source TileJSON](#source-tilejson) | +| `/{source1},…,{sourceN}/{z}/{x}/{y}` | [Composite Source Tiles](30-config-file.md) | +| `/sprite/{spriteID}[@2x].{json,png}` | [Sprite sources](36-sources-spritess.md) | +| `/font/{font}/{start}-{end}` | [Font source](#37-sources-fonts.md) | +| `/font/{font1},…,{fontN}/{start}-{end}` | [Composite Font source](#37-sources-fonts.md) | +| `/health` | Martin server health check: returns 200 `OK` | ### Duplicate Source ID In case there is more than one source that has the same name, e.g. a PG function is available in two schemas/connections, or a table has more than one geometry columns, sources will be assigned unique IDs such as `/points`, `/points.1`, etc. @@ -43,6 +45,25 @@ curl localhost:3000/catalog | jq }, ... }, + "sprites": { + "cool_icons": { + "images": [ + "bicycle", + "bear", + ] + }, + ... + }, + "fonts": { + "Noto Mono Regular": { + "family": "Noto Mono", + "style": "Regular", + "glyphs": 875, + "start": 0, + "end": 65533 + }, + ... + } } ``` From 65b1cdcb83424d07a8f7be06b595de1ca4c8a7d8 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Mon, 30 Oct 2023 18:25:37 -0400 Subject: [PATCH 094/108] more doc fixes --- docs/src/37-sources-fonts.md | 17 +++++++++++++---- docs/src/40-using-endpoints.md | 20 ++++++++++---------- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/docs/src/37-sources-fonts.md b/docs/src/37-sources-fonts.md index 797d0b8c2..97845cb48 100644 --- a/docs/src/37-sources-fonts.md +++ b/docs/src/37-sources-fonts.md @@ -6,13 +6,22 @@ The glyph range generation is not yet cached, and may require external reverse p ## API Fonts ranges are available either for a single font, or a combination of multiple fonts. The font names are case-sensitive and should match the font name in the font file as published in the catalog. Make sure to URL-escape font names as they usually contain spaces. +| | Font Request | +|---------|--------------------------------------| +| Pattern | `/font/{name}/{start}-{end}` | +| Example | `/font/Overpass%20Mono%20Bold/0-255` | + + +### Composite Font Request + When combining multiple fonts, the glyph range will contain glyphs from the first listed font if available, and fallback to the next font if the glyph is not available in the first font, etc. The glyph range will be empty if none of the fonts contain the glyph. -| Type | API | Example | -|----------|------------------------------------------------|--------------------------------------------------------------| -| Single | `/font/{name}/{start}-{end}` | `/font/Overpass%20Mono%20Bold/0-255` | -| Combined | `/font/{name1},{name2},{name_n}/{start}-{end}` | `/font/Overpass%20Mono%20Bold,Overpass%20Mono%20Light/0-255` | +| | Composite Font Request with fallbacks | +|---------|--------------------------------------------------------------| +| Pattern | `/font/{name1},…,{nameN}/{start}-{end}` | +| Example | `/font/Overpass%20Mono%20Bold,Overpass%20Mono%20Light/0-255` | +### Catalog Martin will show all available fonts at the `/catalog` endpoint. ```shell diff --git a/docs/src/40-using-endpoints.md b/docs/src/40-using-endpoints.md index 201750ec0..10df67945 100644 --- a/docs/src/40-using-endpoints.md +++ b/docs/src/40-using-endpoints.md @@ -2,18 +2,18 @@ Martin data is available via the HTTP `GET` endpoints: -| URL | Description | -|-----------------------------------------|-----------------------------------------------| +| URL | Description | +|-----------------------------------------|----------------------------------------------| | `/` | Status text, that will eventually show web UI | -| `/catalog` | [List of all sources](#catalog) | -| `/{sourceID}` | [Source TileJSON](#source-tilejson) | -| `/{sourceID}/{z}/{x}/{y}` | Map Tiles | +| `/catalog` | [List of all sources](#catalog) | +| `/{sourceID}` | [Source TileJSON](#source-tilejson) | +| `/{sourceID}/{z}/{x}/{y}` | Map Tiles | | `/{source1},…,{sourceN}` | [Composite Source TileJSON](#source-tilejson) | -| `/{source1},…,{sourceN}/{z}/{x}/{y}` | [Composite Source Tiles](30-config-file.md) | -| `/sprite/{spriteID}[@2x].{json,png}` | [Sprite sources](36-sources-spritess.md) | -| `/font/{font}/{start}-{end}` | [Font source](#37-sources-fonts.md) | -| `/font/{font1},…,{fontN}/{start}-{end}` | [Composite Font source](#37-sources-fonts.md) | -| `/health` | Martin server health check: returns 200 `OK` | +| `/{source1},…,{sourceN}/{z}/{x}/{y}` | [Composite Source Tiles](30-config-file.md) | +| `/sprite/{spriteID}[@2x].{json,png}` | [Sprite sources](36-sources-spritess.md) | +| `/font/{font}/{start}-{end}` | [Font source](37-sources-fonts.md) | +| `/font/{font1},…,{fontN}/{start}-{end}` | [Composite Font source](37-sources-fonts.md) | +| `/health` | Martin server health check: returns 200 `OK` | ### Duplicate Source ID In case there is more than one source that has the same name, e.g. a PG function is available in two schemas/connections, or a table has more than one geometry columns, sources will be assigned unique IDs such as `/points`, `/points.1`, etc. From bfbe52d0325251c6d59200dda8ff7a7ed123c6db Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Mon, 30 Oct 2023 18:52:21 -0400 Subject: [PATCH 095/108] Add crate categories --- Cargo.lock | 6 +++--- martin-tile-utils/Cargo.toml | 3 ++- martin/Cargo.toml | 3 ++- mbtiles/Cargo.toml | 3 ++- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a1be09bff..86ad530b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1730,7 +1730,7 @@ checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "martin" -version = "0.10.0" +version = "0.10.1" dependencies = [ "actix-cors", "actix-http", @@ -1778,11 +1778,11 @@ dependencies = [ [[package]] name = "martin-tile-utils" -version = "0.1.3" +version = "0.1.4" [[package]] name = "mbtiles" -version = "0.7.1" +version = "0.7.2" dependencies = [ "actix-rt", "anyhow", diff --git a/martin-tile-utils/Cargo.toml b/martin-tile-utils/Cargo.toml index d4f713298..4d60a9d38 100644 --- a/martin-tile-utils/Cargo.toml +++ b/martin-tile-utils/Cargo.toml @@ -1,9 +1,10 @@ [package] name = "martin-tile-utils" -version = "0.1.3" +version = "0.1.4" authors = ["Yuri Astrakhan ", "MapLibre contributors"] description = "Utilites to help with map tile processing, such as type and compression detection. Used by the MapLibre's Martin tile server." keywords = ["maps", "tiles", "mvt", "tileserver"] +categories = ["science::geo", "parsing"] exclude = [ # Exclude the fixtures directory from the package - it's only used for tests. "/fixtures", diff --git a/martin/Cargo.toml b/martin/Cargo.toml index cd9cfb791..16998806a 100644 --- a/martin/Cargo.toml +++ b/martin/Cargo.toml @@ -1,10 +1,11 @@ [package] name = "martin" # Once the release is published with the hash, update https://github.com/maplibre/homebrew-martin -version = "0.10.0" +version = "0.10.1" authors = ["Stepan Kuzmin ", "Yuri Astrakhan ", "MapLibre contributors"] description = "Blazing fast and lightweight tile server with PostGIS, MBTiles, and PMTiles support" keywords = ["maps", "tiles", "mbtiles", "pmtiles", "postgis"] +categories = ["science::geo", "web-programming::http-server"] exclude = [ # Tests include a lot of data and other test files that are not needed for the users of the library "/tests", diff --git a/mbtiles/Cargo.toml b/mbtiles/Cargo.toml index fc1051be1..c31ab35c6 100644 --- a/mbtiles/Cargo.toml +++ b/mbtiles/Cargo.toml @@ -1,9 +1,10 @@ [package] name = "mbtiles" -version = "0.7.1" +version = "0.7.2" authors = ["Yuri Astrakhan ", "MapLibre contributors"] description = "A simple low-level MbTiles access and processing library, with some tile format detection and other relevant heuristics." keywords = ["mbtiles", "maps", "tiles", "mvt", "tilejson"] +categories = ["science::geo", "database"] edition.workspace = true license.workspace = true repository.workspace = true From 3a1f7acc9568527a23b48da53626722a71b790cd Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Mon, 30 Oct 2023 21:52:11 -0400 Subject: [PATCH 096/108] fix image test extension --- .../auto/{mb_jpg_0_0_0.png => mb_jpg_0_0_0.jpg} | Bin .../{mb_jpg_0_0_0.png.txt => mb_jpg_0_0_0.jpg.txt} | 2 +- tests/test.sh | 13 ++++++++++--- 3 files changed, 11 insertions(+), 4 deletions(-) rename tests/expected/auto/{mb_jpg_0_0_0.png => mb_jpg_0_0_0.jpg} (100%) rename tests/expected/auto/{mb_jpg_0_0_0.png.txt => mb_jpg_0_0_0.jpg.txt} (60%) diff --git a/tests/expected/auto/mb_jpg_0_0_0.png b/tests/expected/auto/mb_jpg_0_0_0.jpg similarity index 100% rename from tests/expected/auto/mb_jpg_0_0_0.png rename to tests/expected/auto/mb_jpg_0_0_0.jpg diff --git a/tests/expected/auto/mb_jpg_0_0_0.png.txt b/tests/expected/auto/mb_jpg_0_0_0.jpg.txt similarity index 60% rename from tests/expected/auto/mb_jpg_0_0_0.png.txt rename to tests/expected/auto/mb_jpg_0_0_0.jpg.txt index 9bdac2a77..daa75c631 100644 --- a/tests/expected/auto/mb_jpg_0_0_0.png.txt +++ b/tests/expected/auto/mb_jpg_0_0_0.jpg.txt @@ -1 +1 @@ -tests/output/auto/mb_jpg_0_0_0.png: JPEG image data, JFIF standard 1.01, aspect ratio, density 1x1, segment length 16, baseline, precision 8, 256x256, components 3 +tests/output/auto/mb_jpg_0_0_0.jpg: JPEG image data, JFIF standard 1.01, aspect ratio, density 1x1, segment length 16, baseline, precision 8, 256x256, components 3 diff --git a/tests/test.sh b/tests/test.sh index 2ea687448..83a399716 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -86,7 +86,8 @@ test_pbf() test_png() { - FILENAME="$TEST_OUT_DIR/$1.png" + # 3rd argument is optional, .png by default + FILENAME="$TEST_OUT_DIR/$1.${3:-png}" URL="$MARTIN_URL/$2" echo "Testing $(basename "$FILENAME") from $URL" @@ -97,13 +98,19 @@ test_png() fi } +test_jpg() +{ + test_png $1 $2 jpg +} + + test_font() { FILENAME="$TEST_OUT_DIR/$1.pbf" URL="$MARTIN_URL/$2" echo "Testing $(basename "$FILENAME") from $URL" - $CURL "$URL" > "$FILENAME" + $CURL "$URL" > "$FILENAME" } # Delete a line from a file $1 that matches parameter $2 @@ -230,7 +237,7 @@ test_png pmt_3_4_2 stamen_toner__raster_CC-BY-ODbL_z3/3/4/2 >&2 echo "***** Test server response for MbTiles source *****" test_jsn mb_jpg geography-class-jpg -test_png mb_jpg_0_0_0 geography-class-jpg/0/0/0 +test_jpg mb_jpg_0_0_0 geography-class-jpg/0/0/0 test_jsn mb_png geography-class-png test_png mb_png_0_0_0 geography-class-png/0/0/0 test_jsn mb_mvt world_cities From c6170c59137a4915dd7d2e33d847d431ba8154b7 Mon Sep 17 00:00:00 2001 From: Lucas Date: Tue, 31 Oct 2023 12:41:21 +0800 Subject: [PATCH 097/108] Improve MBTiles documentation (#961) Fixes #918 --------- Co-authored-by: Yuri Astrakhan --- docs/src/50-tools.md | 141 ++---------------------------- docs/src/51-mbtiles-meta.md | 22 +++++ docs/src/52-mbtiles-copy.md | 57 ++++++++++++ docs/src/53-mbtiles-validation.md | 38 ++++++++ docs/src/54-mbtiles-schema.md | 80 +++++++++++++++++ docs/src/SUMMARY.md | 4 + justfile | 6 +- 7 files changed, 213 insertions(+), 135 deletions(-) create mode 100644 docs/src/51-mbtiles-meta.md create mode 100644 docs/src/52-mbtiles-copy.md create mode 100644 docs/src/53-mbtiles-validation.md create mode 100644 docs/src/54-mbtiles-schema.md diff --git a/docs/src/50-tools.md b/docs/src/50-tools.md index e0d5ec06c..129bcb418 100644 --- a/docs/src/50-tools.md +++ b/docs/src/50-tools.md @@ -1,139 +1,12 @@ -# Tools +# CLI Tools -Martin has a few additional tools that can be used to interact with the data. +Martin project contains additional tooling to help manage the data servable with Martin tile server. -## MBTiles tool -A small utility that allows users to interact with the `*.mbtiles` files from the command line. Use `mbtiles --help` to see a list of available commands, and `mbtiles --help` to see help for a specific command. +## `mbtiles` +`mbtiles` is a small utility to interact with the `*.mbtiles` files from the command line. It allows users to examine, copy, validate, compare, and apply diffs between them. -This tool can be installed by compiling the latest released version with `cargo install mbtiles`, or by downloading a pre-built binary from the [releases page](https://github.com/maplibre/martin/releases/latest). - -### meta-all -Print all metadata values to stdout, as well as the results of tile detection. The format of the values printed is not stable, and should only be used for visual inspection. - -```shell -mbtiles meta-all my_file.mbtiles -``` - -### meta-get -Retrieve raw metadata value by its name. The value is printed to stdout without any modifications. For example, to get the `description` value from an mbtiles file: - -```shell -mbtiles meta-get my_file.mbtiles description -``` - -### meta-set -Set metadata value by its name, or delete the key if no value is supplied. For example, to set the `description` value to `A vector tile dataset`: - -```shell -mbtiles meta-set my_file.mbtiles description "A vector tile dataset" -``` - -### copy -Copy an mbtiles file, optionally filtering its content by zoom levels. - -```shell -mbtiles copy src_file.mbtiles dst_file.mbtiles \ - --min-zoom 0 --max-zoom 10 -``` - -Copy command can also be used to compare two mbtiles files and generate a delta (diff) file. The diff file can be applied to the `src_file.mbtiles` elsewhere, to avoid copying/transmitting the entire modified dataset. The delta file will contain all tiles that are different between the two files (modifications, insertions, and deletions as `NULL` values), for both the tile and metadata tables. - -There is one exception: `agg_tiles_hash` metadata value will be renamed to `agg_tiles_hash_in_diff`, and a new `agg_tiles_hash` will be generated for the diff file itself. This is done to avoid confusion when applying the diff file to the original file, as the `agg_tiles_hash` value will be different after the diff is applied. The `apply-diff` command will automatically rename the `agg_tiles_hash_in_diff` value back to `agg_tiles_hash` when applying the diff. - -```shell -mbtiles copy src_file.mbtiles diff_file.mbtiles \ - --diff-with-file modified_file.mbtiles -``` - -This command can also be used to generate files of different [supported schema](##supported-schema). -```shell -mbtiles copy normalized.mbtiles dst.mbtiles \ - --dst-mbttype flat-with-hash -``` -### apply-diff -Apply the diff file generated from `copy` command above to an mbtiles file. The diff file can be applied to the `src_file.mbtiles` elsewhere, to avoid copying/transmitting the entire modified dataset. - -Note that the `agg_tiles_hash_in_diff` metadata value will be renamed to `agg_tiles_hash` when applying the diff. This is done to avoid confusion when applying the diff file to the original file, as the `agg_tiles_hash` value will be different after the diff is applied. +Use `mbtiles --help` to see a list of available commands, and `mbtiles --help` to see help for a specific command. -```shell -mbtiles apply_diff src_file.mbtiles diff_file.mbtiles -``` - -Another way to apply the diff is to use the `sqlite3` command line tool directly. This SQL will delete all tiles from `src_file.mbtiles` that are set to `NULL` in `diff_file.mbtiles`, and then insert or update all new tiles from `diff_file.mbtiles` into `src_file.mbtiles`, where both files are of `flat` type. The name of the diff file is passed as a query parameter to the sqlite3 command line tool, and then used in the SQL statements. -```shell -sqlite3 src_file.mbtiles \ - -bail \ - -cmd ".parameter set @diffDbFilename diff_file.mbtiles" \ - "ATTACH DATABASE @diffDbFilename AS diffDb;" \ - "DELETE FROM tiles WHERE (zoom_level, tile_column, tile_row) IN (SELECT zoom_level, tile_column, tile_row FROM diffDb.tiles WHERE tile_data ISNULL);" \ - "INSERT OR REPLACE INTO tiles (zoom_level, tile_column, tile_row, tile_data) SELECT * FROM diffDb.tiles WHERE tile_data NOTNULL;" -``` - -### validate -If the `.mbtiles` file is of `flat_with_hash` or `normalized` type, then verify that the data stored in columns `tile_hash` and `tile_id` respectively are MD5 hashes of the `tile_data` column. -```shell -mbtiles validate src_file.mbtiles -``` - -## Content Validation -The original [MBTiles specification](https://github.com/mapbox/mbtiles-spec#readme) does not provide any guarantees for the content of the tile data in MBTiles. This tool adds a few additional conventions to ensure that the content of the tile data is valid. - -A typical Normalized schema generated by tools like [tilelive-copy](https://github.com/mapbox/TileLive#bintilelive-copy) use MD5 hash in the `tile_id` column. The Martin's `mbtiles` tool can use this hash to verify the content of each tile. We also define a new `flat-with-hash` schema that stores the hash and tile data in the same table. This schema is more efficient than the `normalized` schema when data has no duplicate tiles (see below). Per tile validation is not available for `flat` schema. - -Per-tile validation will catch individual invalid tiles, but it will not detect overall datastore corruption (e.g. missing tiles or tiles that shouldn't exist, or tiles with incorrect z/x/y values). -For that, Martin `mbtiles` tool defines a new metadata value called `agg_tiles_hash`. The value is computed by hashing `cast(zoom_level AS text), cast(tile_column AS text), cast(tile_row AS text), tile_data` combined for all rows in the `tiles` table/view, ordered by z,x,y. -In case there are no rows or all are NULL, the hash value of an empty string is used. Note that SQLite allows any value type to be stored as in any column, so if `tile_data` accidentally contains non-blob/text/null value, validation will fail. - -The `mbtiles` tool will compute `agg_tiles_hash` value when copying or validating mbtiles files. - -## Supported Schema -The `mbtiles` tool supports three different kinds of schema for `tiles` data in `.mbtiles` files. See also the original [specification](https://github.com/mapbox/mbtiles-spec#readme). - -### flat -```sql, ignore -CREATE TABLE tiles (zoom_level integer, tile_column integer, tile_row integer, tile_data blob); -CREATE UNIQUE INDEX tile_index on tiles (zoom_level, tile_column, tile_row); -``` - -### flat-with-hash -```sql, ignore -CREATE TABLE tiles_with_hash ( - zoom_level integer NOT NULL, - tile_column integer NOT NULL, - tile_row integer NOT NULL, - tile_data blob, - tile_hash text); -CREATE UNIQUE INDEX tiles_with_hash_index on tiles_with_hash (zoom_level, tile_column, tile_row); -CREATE VIEW tiles AS SELECT zoom_level, tile_column, tile_row, tile_data FROM tiles_with_hash; -``` - -### normalized -```sql, ignore -CREATE TABLE map (zoom_level INTEGER, tile_column INTEGER, tile_row INTEGER, tile_id TEXT); -CREATE UNIQUE INDEX map_index ON map (zoom_level, tile_column, tile_row); -CREATE TABLE images (tile_id text, tile_data blob); -CREATE UNIQUE INDEX images_id ON images (tile_id); -CREATE VIEW tiles AS - SELECT - map.zoom_level AS zoom_level, - map.tile_column AS tile_column, - map.tile_row AS tile_row, - images.tile_data AS tile_data - FROM map - JOIN images ON images.tile_id = map.tile_id; -``` - -Optionally, `.mbtiles` files with `normalized` schema can include a `tiles_with_hash` view: - -```sql, ignore -CREATE VIEW tiles_with_hash AS - SELECT - map.zoom_level AS zoom_level, - map.tile_column AS tile_column, - map.tile_row AS tile_row, - images.tile_data AS tile_data, - images.tile_id AS tile_hash - FROM map LEFT JOIN images ON map.tile_id = images.tile_id; -``` +This tool can be installed by compiling the latest released version with `cargo install mbtiles`, or by downloading a pre-built binary from the [releases page](https://github.com/maplibre/martin/releases/latest). -**__Note:__** All `normalized` files created by the `mbtiles` tool will contain this view. +The `mbtiles` utility builds on top of the [MBTiles specification](https://github.com/mapbox/mbtiles-spec). It adds a few additional conventions to ensure that the content of the tile data is valid, and can be used for reliable diffing and patching of the tilesets. diff --git a/docs/src/51-mbtiles-meta.md b/docs/src/51-mbtiles-meta.md new file mode 100644 index 000000000..65930cb4c --- /dev/null +++ b/docs/src/51-mbtiles-meta.md @@ -0,0 +1,22 @@ +# `mbtiles` Metadata Access + +## meta-all +Print all metadata values to stdout, as well as the results of tile detection. The format of the values printed is not stable, and should only be used for visual inspection. + +```shell +mbtiles meta-all my_file.mbtiles +``` + +## meta-get +Retrieve raw metadata value by its name. The value is printed to stdout without any modifications. For example, to get the `description` value from an mbtiles file: + +```shell +mbtiles meta-get my_file.mbtiles description +``` + +## meta-set +Set metadata value by its name, or delete the key if no value is supplied. For example, to set the `description` value to `A vector tile dataset`: + +```shell +mbtiles meta-set my_file.mbtiles description "A vector tile dataset" +``` diff --git a/docs/src/52-mbtiles-copy.md b/docs/src/52-mbtiles-copy.md new file mode 100644 index 000000000..9057a3ac4 --- /dev/null +++ b/docs/src/52-mbtiles-copy.md @@ -0,0 +1,57 @@ +# Copying, Diffing, and Patching MBTiles + +## `mbtiles copy` +Copy command copies an mbtiles file, optionally filtering its content by zoom levels. + +```shell +mbtiles copy src_file.mbtiles dst_file.mbtiles \ + --min-zoom 0 --max-zoom 10 +``` + +This command can also be used to generate files of different [supported schema](##supported-schema). + +```shell +mbtiles copy normalized.mbtiles dst.mbtiles \ + --dst-mbttype flat-with-hash +``` + +## `mbtiles copy --diff-with-file` +Copy command can also be used to compare two mbtiles files and generate a delta (diff) file. The diff file can be applied to the `src_file.mbtiles` elsewhere, to avoid copying/transmitting the entire modified dataset. The delta file will contain all tiles that are different between the two files (modifications, insertions, and deletions as `NULL` values), for both the tile and metadata tables. + +There is one exception: `agg_tiles_hash` metadata value will be renamed to `agg_tiles_hash_in_diff`, and a new `agg_tiles_hash` will be generated for the diff file itself. This is done to avoid confusion when applying the diff file to the original file, as the `agg_tiles_hash` value will be different after the diff is applied. The `apply-diff` command will automatically rename the `agg_tiles_hash_in_diff` value back to `agg_tiles_hash` when applying the diff. + +```shell +mbtiles copy src_file.mbtiles diff_file.mbtiles \ + --diff-with-file modified_file.mbtiles +``` + +## `mbtiles copy --apply-patch` + +Copy a source file to destination while also applying the diff file generated by `copy --diff-with-file` command above to the destination mbtiles file. This allows safer application of the diff file, as the source file is not modified. + +```shell +mbtiles copy src_file.mbtiles dst_file.mbtiles \ + --apply-patch diff_file.mbtiles +``` + +## `mbtiles apply-patch` + +Apply the diff file generated from `copy` command above to an mbtiles file. The diff file can be applied to the `src_file.mbtiles` elsewhere, to avoid copying/transmitting the entire modified dataset. + +Note that the `agg_tiles_hash_in_diff` metadata value will be renamed to `agg_tiles_hash` when applying the diff. This is done to avoid confusion when applying the diff file to the original file, as the `agg_tiles_hash` value will be different after the diff is applied. + +```shell +mbtiles apply_diff src_file.mbtiles diff_file.mbtiles +``` + +#### Applying diff with SQLite +Another way to apply the diff is to use the `sqlite3` command line tool directly. This SQL will delete all tiles from `src_file.mbtiles` that are set to `NULL` in `diff_file.mbtiles`, and then insert or update all new tiles from `diff_file.mbtiles` into `src_file.mbtiles`, where both files are of `flat` type. The name of the diff file is passed as a query parameter to the sqlite3 command line tool, and then used in the SQL statements. Note that this does not update the `agg_tiles_hash` metadata value, so it will be incorrect after the diff is applied. + +```shell +sqlite3 src_file.mbtiles \ + -bail \ + -cmd ".parameter set @diffDbFilename diff_file.mbtiles" \ + "ATTACH DATABASE @diffDbFilename AS diffDb;" \ + "DELETE FROM tiles WHERE (zoom_level, tile_column, tile_row) IN (SELECT zoom_level, tile_column, tile_row FROM diffDb.tiles WHERE tile_data ISNULL);" \ + "INSERT OR REPLACE INTO tiles (zoom_level, tile_column, tile_row, tile_data) SELECT * FROM diffDb.tiles WHERE tile_data NOTNULL;" +``` diff --git a/docs/src/53-mbtiles-validation.md b/docs/src/53-mbtiles-validation.md new file mode 100644 index 000000000..65c3f58b3 --- /dev/null +++ b/docs/src/53-mbtiles-validation.md @@ -0,0 +1,38 @@ +# MBTiles Validation + +The original [MBTiles specification](https://github.com/mapbox/mbtiles-spec#readme) does not provide any guarantees for the content of the tile data in MBTiles. `mbtiles validate` assumes a few additional conventions and uses them to ensure that the content of the tile data is valid performing several validation steps. If the file is not valid, the command will print an error message and exit with a non-zero exit code. + +```shell +mbtiles validate src_file.mbtiles +``` + +## SQLite Integrity check +The `validate` command will run `PRAGMA integrity_check` on the file, and will fail if the result is not `ok`. The `--integrity-check` flag can be used to disable this check, or to make it more thorow with `full` value. Default is `quick`. + +## Schema check +The `validate` command will verify that the `tiles` table/view exists, and that it has the expected columns and indexes. It will also verify that the `metadata` table/view exists, and that it has the expected columns and indexes. + +## Per-tile validation +If the `.mbtiles` file uses [flat_with_hash](54-mbtiles-schema.md#flat-with-hash) or [normalized](54-mbtiles-schema.md#normalized) schema, the `validate` command will verify that the MD5 hash of the `tile_data` column matches the `tile_hash` or `tile_id` columns (depending on the schema). + +A typical Normalized schema generated by tools like [tilelive-copy](https://github.com/mapbox/TileLive#bintilelive-copy) use MD5 hash in the `tile_id` column. The Martin's `mbtiles` tool can use this hash to verify the content of each tile. We also define a new [flat-with-hash](54-mbtiles-schema.md#flat-with-hash) schema that stores the hash and tile data in the same table, allowing per-tile validation without the multiple table layout. + +Per-tile validation is not available for the `flat` schema, and will be skipped. + +## Aggregate Content Validation + +Per-tile validation will catch individual tile corruption, but it will not detect overall datastore corruption such as missing tiles, tiles that should not exist, or tiles with incorrect z/x/y values. For that, the `mbtiles` tool defines a new metadata value called `agg_tiles_hash`. + +The value is computed by hashing the combined value for all rows in the `tiles` table/view, ordered by z,x,y. The value is computed using the following SQL expression, which uses a custom `md5_concat_hex` function from [sqlite-hashes crate](https://crates.io/crates/sqlite-hashes): + +```sql, ignore +md5_concat_hex( + CAST(zoom_level AS TEXT), + CAST(tile_column AS TEXT), + CAST(tile_row AS TEXT), + tile_data) +``` + +In case there are no rows or all are NULL, the hash value of an empty string is used. Note that SQLite allows any value type to be stored as in any column, so if `tile_data` accidentally contains non-blob/text/null value, validation will fail. + +The `mbtiles` tool will compute `agg_tiles_hash` value when copying or validating mbtiles files. Use `--update-agg-tiles-hash` to force the value to be updated, even if it is incorrect or does not exist. diff --git a/docs/src/54-mbtiles-schema.md b/docs/src/54-mbtiles-schema.md new file mode 100644 index 000000000..b840392a1 --- /dev/null +++ b/docs/src/54-mbtiles-schema.md @@ -0,0 +1,80 @@ +# MBTiles Schemas +The `mbtiles` tool builds on top of the original [MBTiles specification](https://github.com/mapbox/mbtiles-spec#readme) by specifying three different kinds of schema for `tiles` data: `flat`, `flat-with-hash`, and `normalized`. The `mbtiles` tool can convert between these schemas, and can also generate a diff between two files of any schemas, as well as merge multiple schema files into one file. + +## flat +Flat schema is the closest to the original MBTiles specification. It stores all tiles in a single table. This schema is the most efficient when the tileset contains no duplicate tiles. + +```sql, ignore +CREATE TABLE tiles ( + zoom_level INTEGER, + tile_column INTEGER, + tile_row INTEGER, + tile_data BLOB); + +CREATE UNIQUE INDEX tile_index on tiles ( + zoom_level, tile_column, tile_row); +``` + +## flat-with-hash +Similar to the `flat` schema, but also includes a `tile_hash` column that contains a hash value of the `tile_data` column. Use this schema when the tileset has no duplicate tiles, but you still want to be able to validate the content of each tile individually. + +```sql, ignore +CREATE TABLE tiles_with_hash ( + zoom_level INTEGER NOT NULL, + tile_column INTEGER NOT NULL, + tile_row INTEGER NOT NULL, + tile_data BLOB, + tile_hash TEXT); + +CREATE UNIQUE INDEX tiles_with_hash_index on tiles_with_hash ( + zoom_level, tile_column, tile_row); + +CREATE VIEW tiles AS + SELECT zoom_level, tile_column, tile_row, tile_data + FROM tiles_with_hash; +``` + +## normalized +Normalized schema is the most efficient when the tileset contains duplicate tiles. It stores all tile blobs in the `images` table, and stores the tile Z,X,Y coordinates in a `map` table. The `map` table contains a `tile_id` column that is a foreign key to the `images` table. The `tile_id` column is a hash of the `tile_data` column, making it possible to both validate each individual tile like in the `flat-with-hash` schema, and also to optimize storage by storing each unique tile only once. + +```sql, ignore +CREATE TABLE map ( + zoom_level INTEGER, + tile_column INTEGER, + tile_row INTEGER, + tile_id TEXT); + +CREATE TABLE images ( + tile_id TEXT, + tile_data BLOB); + +CREATE UNIQUE INDEX map_index ON map ( + zoom_level, tile_column, tile_row); +CREATE UNIQUE INDEX images_id ON images ( + tile_id); + +CREATE VIEW tiles AS + SELECT + map.zoom_level AS zoom_level, + map.tile_column AS tile_column, + map.tile_row AS tile_row, + images.tile_data AS tile_data + FROM + map JOIN images + ON images.tile_id = map.tile_id; +``` + +Optionally, `.mbtiles` files with `normalized` schema can include a `tiles_with_hash` view. All `normalized` files created by the `mbtiles` tool will contain this view. + +```sql, ignore +CREATE VIEW tiles_with_hash AS + SELECT + map.zoom_level AS zoom_level, + map.tile_column AS tile_column, + map.tile_row AS tile_row, + images.tile_data AS tile_data, + images.tile_id AS tile_hash + FROM + map JOIN images + ON map.tile_id = images.tile_id; +``` diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 018f59937..fbfd32f21 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -22,4 +22,8 @@ - [Using with Mapbox](44-using-with-mapbox.md) - [Recipes](45-recipes.md) - [Tools](50-tools.md) + - [MBTiles Metadata](51-mbtiles-meta.md) + - [MBTiles Copying / Diffing](52-mbtiles-copy.md) + - [MBTiles Validation](53-mbtiles-validation.md) + - [MBTiles Schemas](54-mbtiles-schema.md) - [Development](60-development.md) diff --git a/justfile b/justfile index 2f3862a3b..873c8aa9b 100644 --- a/justfile +++ b/justfile @@ -16,7 +16,11 @@ export CARGO_TERM_COLOR := "always" # Start Martin server run *ARGS: - cargo run -- {{ ARGS }} + cargo run -p martin -- {{ ARGS }} + +# Run mbtiles command +mbtiles *ARGS: + cargo run -p mbtiles -- {{ ARGS }} # Start release-compiled Martin server and a test database run-release *ARGS: start From d9f01d15d4aacd321fd15ea7f196b63268d8862c Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Tue, 31 Oct 2023 00:53:39 -0400 Subject: [PATCH 098/108] cleanup docs --- docs/src/54-mbtiles-schema.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/src/54-mbtiles-schema.md b/docs/src/54-mbtiles-schema.md index b840392a1..188196b48 100644 --- a/docs/src/54-mbtiles-schema.md +++ b/docs/src/54-mbtiles-schema.md @@ -20,11 +20,11 @@ Similar to the `flat` schema, but also includes a `tile_hash` column that contai ```sql, ignore CREATE TABLE tiles_with_hash ( - zoom_level INTEGER NOT NULL, + zoom_level INTEGER NOT NULL, tile_column INTEGER NOT NULL, - tile_row INTEGER NOT NULL, - tile_data BLOB, - tile_hash TEXT); + tile_row INTEGER NOT NULL, + tile_data BLOB, + tile_hash TEXT); CREATE UNIQUE INDEX tiles_with_hash_index on tiles_with_hash ( zoom_level, tile_column, tile_row); @@ -39,13 +39,13 @@ Normalized schema is the most efficient when the tileset contains duplicate tile ```sql, ignore CREATE TABLE map ( - zoom_level INTEGER, + zoom_level INTEGER, tile_column INTEGER, - tile_row INTEGER, - tile_id TEXT); + tile_row INTEGER, + tile_id TEXT); CREATE TABLE images ( - tile_id TEXT, + tile_id TEXT, tile_data BLOB); CREATE UNIQUE INDEX map_index ON map ( @@ -55,9 +55,9 @@ CREATE UNIQUE INDEX images_id ON images ( CREATE VIEW tiles AS SELECT - map.zoom_level AS zoom_level, - map.tile_column AS tile_column, - map.tile_row AS tile_row, + map.zoom_level AS zoom_level, + map.tile_column AS tile_column, + map.tile_row AS tile_row, images.tile_data AS tile_data FROM map JOIN images @@ -69,11 +69,11 @@ Optionally, `.mbtiles` files with `normalized` schema can include a `tiles_with_ ```sql, ignore CREATE VIEW tiles_with_hash AS SELECT - map.zoom_level AS zoom_level, - map.tile_column AS tile_column, - map.tile_row AS tile_row, + map.zoom_level AS zoom_level, + map.tile_column AS tile_column, + map.tile_row AS tile_row, images.tile_data AS tile_data, - images.tile_id AS tile_hash + images.tile_id AS tile_hash FROM map JOIN images ON map.tile_id = images.tile_id; From 09dd2bea629e53e8903703f3ed683ec877d4f2e9 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Tue, 31 Oct 2023 00:56:13 -0400 Subject: [PATCH 099/108] cleanup trailing spaces --- .github/workflows/ci.yml | 2 +- demo/frontend/src/Components/Map/Filters/DayPicker.ts | 2 +- docs/src/37-sources-fonts.md | 2 +- docs/src/50-tools.md | 4 ++-- docs/src/53-mbtiles-validation.md | 2 +- mbtiles/tests/mbtiles.rs | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ad706b479..3188b51d0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -570,7 +570,7 @@ jobs: cd .. # - # Special case for Windows + # Special case for Windows # cd x86_64-pc-windows-msvc 7z a ../files/martin-Windows-x86_64.zip martin.exe mbtiles.exe diff --git a/demo/frontend/src/Components/Map/Filters/DayPicker.ts b/demo/frontend/src/Components/Map/Filters/DayPicker.ts index 655b00a00..e5abda1b3 100644 --- a/demo/frontend/src/Components/Map/Filters/DayPicker.ts +++ b/demo/frontend/src/Components/Map/Filters/DayPicker.ts @@ -11,7 +11,7 @@ export default styled.div` } .DayPicker-Caption > div { - font-weight: bold; + font-weight: bold; color: #dadfee; } diff --git a/docs/src/37-sources-fonts.md b/docs/src/37-sources-fonts.md index 97845cb48..65f055a9c 100644 --- a/docs/src/37-sources-fonts.md +++ b/docs/src/37-sources-fonts.md @@ -1,7 +1,7 @@ ## Font Sources Martin can serve glyph ranges from `otf`, `ttf`, and `ttc` fonts as needed by MapLibre text rendering. Martin will generate them dynamically on the fly. -The glyph range generation is not yet cached, and may require external reverse proxy or CDN for faster operation. +The glyph range generation is not yet cached, and may require external reverse proxy or CDN for faster operation. ## API Fonts ranges are available either for a single font, or a combination of multiple fonts. The font names are case-sensitive and should match the font name in the font file as published in the catalog. Make sure to URL-escape font names as they usually contain spaces. diff --git a/docs/src/50-tools.md b/docs/src/50-tools.md index 129bcb418..f581affec 100644 --- a/docs/src/50-tools.md +++ b/docs/src/50-tools.md @@ -1,4 +1,4 @@ -# CLI Tools +# CLI Tools Martin project contains additional tooling to help manage the data servable with Martin tile server. @@ -9,4 +9,4 @@ Use `mbtiles --help` to see a list of available commands, and `mbtiles This tool can be installed by compiling the latest released version with `cargo install mbtiles`, or by downloading a pre-built binary from the [releases page](https://github.com/maplibre/martin/releases/latest). -The `mbtiles` utility builds on top of the [MBTiles specification](https://github.com/mapbox/mbtiles-spec). It adds a few additional conventions to ensure that the content of the tile data is valid, and can be used for reliable diffing and patching of the tilesets. +The `mbtiles` utility builds on top of the [MBTiles specification](https://github.com/mapbox/mbtiles-spec). It adds a few additional conventions to ensure that the content of the tile data is valid, and can be used for reliable diffing and patching of the tilesets. diff --git a/docs/src/53-mbtiles-validation.md b/docs/src/53-mbtiles-validation.md index 65c3f58b3..e2a30851c 100644 --- a/docs/src/53-mbtiles-validation.md +++ b/docs/src/53-mbtiles-validation.md @@ -7,7 +7,7 @@ mbtiles validate src_file.mbtiles ``` ## SQLite Integrity check -The `validate` command will run `PRAGMA integrity_check` on the file, and will fail if the result is not `ok`. The `--integrity-check` flag can be used to disable this check, or to make it more thorow with `full` value. Default is `quick`. +The `validate` command will run `PRAGMA integrity_check` on the file, and will fail if the result is not `ok`. The `--integrity-check` flag can be used to disable this check, or to make it more thorow with `full` value. Default is `quick`. ## Schema check The `validate` command will verify that the `tiles` table/view exists, and that it has the expected columns and indexes. It will also verify that the `metadata` table/view exists, and that it has the expected columns and indexes. diff --git a/mbtiles/tests/mbtiles.rs b/mbtiles/tests/mbtiles.rs index 8782cda8f..e15031004 100644 --- a/mbtiles/tests/mbtiles.rs +++ b/mbtiles/tests/mbtiles.rs @@ -15,7 +15,7 @@ use sqlx::{query, query_as, Executor as _, Row, SqliteConnection}; const TILES_V1: &str = " INSERT INTO tiles (zoom_level, tile_column, tile_row, tile_data) VALUES - --(z, x, y, data) -- rules: keep if x=0, edit if x=1, remove if x=2 + --(z, x, y, data) -- rules: keep if x=0, edit if x=1, remove if x=2 (5, 0, 0, cast('same' as blob)) , (5, 0, 1, cast('' as blob)) -- empty tile, keep , (5, 1, 1, cast('edit-v1' as blob)) From 73b56e8a90f3617ca04a09f2438dd8a54593ef20 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Tue, 31 Oct 2023 14:04:10 -0400 Subject: [PATCH 100/108] Include readme as docs for all crates (#981) This adds documentation in the https://docs.rs site. --- martin-tile-utils/src/lib.rs | 2 ++ martin/src/lib.rs | 1 + mbtiles/src/lib.rs | 1 + 3 files changed, 4 insertions(+) diff --git a/martin-tile-utils/src/lib.rs b/martin-tile-utils/src/lib.rs index 5ff13b003..ceb8a5ab2 100644 --- a/martin-tile-utils/src/lib.rs +++ b/martin-tile-utils/src/lib.rs @@ -1,3 +1,5 @@ +#![doc = include_str!("../README.md")] + // This code was partially adapted from https://github.com/maplibre/mbtileserver-rs // project originally written by Kaveh Karimi and licensed under MIT/Apache-2.0 diff --git a/martin/src/lib.rs b/martin/src/lib.rs index 827fa036a..1feb9843c 100644 --- a/martin/src/lib.rs +++ b/martin/src/lib.rs @@ -1,3 +1,4 @@ +#![doc = include_str!("../README.md")] #![forbid(unsafe_code)] #![warn(clippy::pedantic)] // Bounds struct derives PartialEq, but not Eq, diff --git a/mbtiles/src/lib.rs b/mbtiles/src/lib.rs index 434fb6a92..0c17da7b1 100644 --- a/mbtiles/src/lib.rs +++ b/mbtiles/src/lib.rs @@ -1,3 +1,4 @@ +#![doc = include_str!("../README.md")] #![allow(clippy::missing_errors_doc)] mod errors; From ccadb56be41d23c5f5ff8f91d76d0ef4113ab494 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Sat, 4 Nov 2023 15:57:10 -0400 Subject: [PATCH 101/108] cargo bump, rm unused font errors --- Cargo.lock | 54 ++++++++++++++++++++--------------------- martin/src/fonts/mod.rs | 9 +------ 2 files changed, 28 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 86ad530b9..eae4c3427 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -699,9 +699,9 @@ dependencies = [ [[package]] name = "crc-catalog" -version = "2.2.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cace84e55f07e7301bae1c519df89cdad8cc3cd868413d3fdbdeca9ff3db484" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crc32fast" @@ -1047,9 +1047,9 @@ checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] name = "fdeflate" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d329bdeac514ee06249dabc27877490f17f5d371ec693360768b838e19f3ae10" +checksum = "64d6dafc854908ff5da46ff3f8f473c6984119a2876a383a860246dd7841a868" dependencies = [ "simd-adler32", ] @@ -1518,9 +1518,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.0.2" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" dependencies = [ "equivalent", "hashbrown 0.14.2", @@ -1599,9 +1599,9 @@ checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" [[package]] name = "js-sys" -version = "0.3.64" +version = "0.3.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" dependencies = [ "wasm-bindgen", ] @@ -2874,7 +2874,7 @@ version = "1.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" dependencies = [ - "indexmap 2.0.2", + "indexmap 2.1.0", "itoa", "ryu", "serde", @@ -2919,7 +2919,7 @@ version = "0.9.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3cc7a1570e38322cfe4154732e5110f887ea57e22b76f4bfd32b5bdd3368666c" dependencies = [ - "indexmap 2.0.2", + "indexmap 2.1.0", "itoa", "ryu", "serde", @@ -3126,7 +3126,7 @@ dependencies = [ "futures-util", "hashlink", "hex", - "indexmap 2.0.2", + "indexmap 2.1.0", "log", "memchr", "once_cell", @@ -3909,9 +3909,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.87" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -3919,9 +3919,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.87" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" dependencies = [ "bumpalo", "log", @@ -3934,9 +3934,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.87" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3944,9 +3944,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.87" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" dependencies = [ "proc-macro2", "quote", @@ -3957,15 +3957,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.87" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" +checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" [[package]] name = "web-sys" -version = "0.3.64" +version = "0.3.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +checksum = "5db499c5f66323272151db0e666cd34f78617522fb0c1604d31a27c50c206a85" dependencies = [ "js-sys", "wasm-bindgen", @@ -4200,18 +4200,18 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" [[package]] name = "zerocopy" -version = "0.7.20" +version = "0.7.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd66a62464e3ffd4e37bd09950c2b9dd6c4f8767380fabba0d523f9a775bc85a" +checksum = "8cd369a67c0edfef15010f980c3cbe45d7f651deac2cd67ce097cd801de16557" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.20" +version = "0.7.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "255c4596d41e6916ced49cfafea18727b24d67878fa180ddfd69b9df34fd1726" +checksum = "c2f140bda219a26ccc0cdb03dba58af72590c53b22642577d88a927bc5c87d6b" dependencies = [ "proc-macro2", "quote", diff --git a/martin/src/fonts/mod.rs b/martin/src/fonts/mod.rs index 5f286e441..1edf21016 100644 --- a/martin/src/fonts/mod.rs +++ b/martin/src/fonts/mod.rs @@ -14,7 +14,6 @@ use pbf_font_tools::{render_sdf_glyph, Fontstack, Glyphs, PbfFontError}; use regex::Regex; use serde::{Deserialize, Serialize}; -use crate::fonts::FontError::IoError; use crate::OptOneMany; const MAX_UNICODE_CP: usize = 0xFFFF; @@ -60,12 +59,6 @@ pub enum FontError { #[error("No font files found in {}", .0.display())] NoFontFilesFound(PathBuf), - #[error("Font {} could not be loaded", .0.display())] - UnableToReadFont(PathBuf), - - #[error("{0} in file {}", .1.display())] - FontProcessingError(spreet::error::Error, PathBuf), - #[error("Font {0} is missing a family name")] MissingFamilyName(PathBuf), @@ -254,7 +247,7 @@ fn recurse_dirs( if path.is_dir() { for dir_entry in path .read_dir() - .map_err(|e| IoError(e, path.clone()))? + .map_err(|e| FontError::IoError(e, path.clone()))? .flatten() { recurse_dirs(lib, dir_entry.path(), fonts, false)?; From 9ddbbe0dcbdb060debed30b413c10056126f1119 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Sun, 5 Nov 2023 01:00:50 -0500 Subject: [PATCH 102/108] Downgrade nightly CI until rustc is fixed (#985) See https://github.com/rust-lang/rust/issues/117598 --- .github/workflows/grcov.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/grcov.yml b/.github/workflows/grcov.yml index 9123bf8c4..d0fec67cb 100644 --- a/.github/workflows/grcov.yml +++ b/.github/workflows/grcov.yml @@ -51,7 +51,7 @@ jobs: - name: Install nightly toolchain uses: dtolnay/rust-toolchain@master with: - toolchain: nightly + toolchain: nightly-2023-11-03 override: true - name: Cleanup GCDA files @@ -78,6 +78,6 @@ jobs: - name: Check conditional cfg values run: | - cargo +nightly check -Z unstable-options -Z check-cfg --workspace + cargo check -Z unstable-options -Z check-cfg --workspace env: RUSTFLAGS: '-D warnings' From fe79756097fe8bcc816ffcca1c293768f37b6acd Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Sun, 5 Nov 2023 21:19:29 -0500 Subject: [PATCH 103/108] Bump lock, improve test.sh --- Cargo.lock | 46 +++++++++++++++++++++++----------------------- tests/test.sh | 50 +++++++++++++++++++++++++++----------------------- 2 files changed, 50 insertions(+), 46 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eae4c3427..71356d944 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -80,7 +80,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -194,7 +194,7 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -351,7 +351,7 @@ checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -362,7 +362,7 @@ checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -598,7 +598,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -826,7 +826,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37e366bff8cd32dd8754b0991fb66b279dc48f598c3a18914852a6673deef583" dependencies = [ "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -978,7 +978,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -1260,7 +1260,7 @@ checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -1644,9 +1644,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.149" +version = "0.2.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" [[package]] name = "libdeflate-sys" @@ -2065,7 +2065,7 @@ dependencies = [ "regex", "regex-syntax 0.7.5", "structmeta", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -2658,7 +2658,7 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.38", + "syn 2.0.39", "unicode-ident", ] @@ -2865,7 +2865,7 @@ checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -3320,7 +3320,7 @@ dependencies = [ "proc-macro2", "quote", "structmeta-derive", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -3331,7 +3331,7 @@ checksum = "a60bcaff7397072dca0017d1db428e30d5002e00b6847703e2e42005c95fbe00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -3375,9 +3375,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.38" +version = "2.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" dependencies = [ "proc-macro2", "quote", @@ -3429,7 +3429,7 @@ checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -3550,7 +3550,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -3657,7 +3657,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -3928,7 +3928,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", "wasm-bindgen-shared", ] @@ -3950,7 +3950,7 @@ checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -4215,7 +4215,7 @@ checksum = "c2f140bda219a26ccc0cdb03dba58af72590c53b22642577d88a927bc5c87d6b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] diff --git a/tests/test.sh b/tests/test.sh index 83a399716..9e76449d8 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -16,49 +16,54 @@ MBTILES_BIN="${MBTILES_BIN:-target/debug/mbtiles}" LOG_DIR="${LOG_DIR:-target/test_logs}" mkdir -p "$LOG_DIR" -function wait_for_martin { +function wait_for { # Seems the --retry-all-errors option is not available on older curl versions, but maybe in the future we can just use this: # timeout -k 20s 20s curl --retry 10 --retry-all-errors --retry-delay 1 -sS "$MARTIN_URL/health" PROCESS_ID=$1 - echo "Waiting for Martin ($PROCESS_ID) to start by checking $MARTIN_URL/health to be valid..." + PROC_NAME=$2 + TEST_URL=$3 + echo "Waiting for $PROC_NAME ($PROCESS_ID) to start by checking $TEST_URL to be valid..." for i in {1..60}; do - if $CURL "$MARTIN_URL/health" 2>/dev/null >/dev/null; then - echo "Martin is up!" - $CURL "$MARTIN_URL/health" + if $CURL "$TEST_URL" 2>/dev/null >/dev/null; then + echo "$PROC_NAME is up!" + if [[ "$PROC_NAME" == "Martin" ]]; then + $CURL "$TEST_URL" + fi return fi if ps -p $PROCESS_ID > /dev/null ; then - echo "Martin is not up yet, waiting for $MARTIN_URL/health ..." + echo "$PROC_NAME is not up yet, waiting for $TEST_URL ..." sleep 1 else - echo "Martin died!" + echo "$PROC_NAME died!" ps au - lsof -i || true + lsof -i || true; exit 1 fi done - echo "Martin did not start in time" + echo "$PROC_NAME did not start in time" ps au - lsof -i || true + lsof -i || true; exit 1 } function kill_process { PROCESS_ID=$1 - echo "Waiting for Martin ($PROCESS_ID) to stop..." + PROC_NAME=$2 + echo "Waiting for $PROC_NAME ($PROCESS_ID) to stop..." kill $PROCESS_ID for i in {1..50}; do if ps -p $PROCESS_ID > /dev/null ; then sleep 0.1 else - echo "Martin ($PROCESS_ID) has stopped" + echo "$PROC_NAME ($PROCESS_ID) has stopped" return fi done - echo "Martin did not stop in time, killing it" + echo "$PROC_NAME did not stop in time, killing it" kill -9 $PROCESS_ID # wait for it to die using timeout and wait - timeout -k 1s 1s wait $PROCESS_ID || true + timeout -k 1s 1s wait $PROCESS_ID || true; } test_jsn() @@ -173,11 +178,11 @@ mkdir -p "$TEST_OUT_DIR" ARG=(--default-srid 900913 --auto-bounds calc --save-config "$(dirname "$0")/output/generated_config.yaml" tests/fixtures/mbtiles tests/fixtures/pmtiles --sprite tests/fixtures/sprites/src1 --font tests/fixtures/fonts/overpass-mono-regular.ttf --font tests/fixtures/fonts) set -x $MARTIN_BIN "${ARG[@]}" 2>&1 | tee "${LOG_DIR}/test_log_1.txt" & -PROCESS_ID=`jobs -p` +MARTIN_PROC_ID=`jobs -p | tail -n 1` { set +x; } 2> /dev/null -trap "kill -9 $PROCESS_ID 2> /dev/null || true" EXIT -wait_for_martin $PROCESS_ID +trap "echo 'Stopping Martin server $MARTIN_PROC_ID...'; kill -9 $MARTIN_PROC_ID 2> /dev/null || true; echo 'Stopped Martin server $MARTIN_PROC_ID';" EXIT HUP INT TERM +wait_for $MARTIN_PROC_ID Martin "$MARTIN_URL/health" >&2 echo "Test catalog" test_jsn catalog_auto catalog @@ -246,7 +251,7 @@ test_pbf mb_mvt_2_3_1 world_cities/2/3/1 >&2 echo "***** Test server response for table source with empty SRID *****" test_pbf points_empty_srid_0_0_0 points_empty_srid/0/0/0 -kill_process $PROCESS_ID +kill_process $MARTIN_PROC_ID Martin validate_log "${LOG_DIR}/test_log_1.txt" @@ -258,10 +263,10 @@ mkdir -p "$TEST_OUT_DIR" ARG=(--config tests/config.yaml --max-feature-count 1000 --save-config "$(dirname "$0")/output/given_config.yaml" -W 1) set -x $MARTIN_BIN "${ARG[@]}" 2>&1 | tee "${LOG_DIR}/test_log_2.txt" & -PROCESS_ID=`jobs -p` +MARTIN_PROC_ID=`jobs -p | tail -n 1` { set +x; } 2> /dev/null -trap "kill -9 $PROCESS_ID 2> /dev/null || true" EXIT -wait_for_martin $PROCESS_ID +trap "echo 'Stopping Martin server $MARTIN_PROC_ID...'; kill -9 $MARTIN_PROC_ID 2> /dev/null || true; echo 'Stopped Martin server $MARTIN_PROC_ID';" EXIT HUP INT TERM +wait_for $MARTIN_PROC_ID Martin "$MARTIN_URL/health" >&2 echo "Test catalog" test_jsn catalog_cfg catalog @@ -289,13 +294,12 @@ test_font font_1 font/Overpass%20Mono%20Light/0-255 test_font font_2 font/Overpass%20Mono%20Regular/0-255 test_font font_3 font/Overpass%20Mono%20Regular,Overpass%20Mono%20Light/0-255 -kill_process $PROCESS_ID +kill_process $MARTIN_PROC_ID Martin validate_log "${LOG_DIR}/test_log_2.txt" remove_line "$(dirname "$0")/output/given_config.yaml" " connection_string: " remove_line "$(dirname "$0")/output/generated_config.yaml" " connection_string: " - echo "------------------------------------------------------------------------------------------------------------------------" echo "Test mbtiles utility" if [[ "$MBTILES_BIN" != "-" ]]; then From faa255431ad6811441cc91f4b726a9ebc163bb93 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Sun, 5 Nov 2023 21:50:26 -0500 Subject: [PATCH 104/108] Simplify README --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 99461909e..70264c034 100755 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Martin +![Martin](https://raw.githubusercontent.com/maplibre/martin/main/logo.png) [![Book](https://img.shields.io/badge/docs-Book-informational)](https://maplibre.org/martin) [![docs.rs docs](https://docs.rs/martin/badge.svg)](https://docs.rs/martin) @@ -13,8 +13,6 @@ Martin is a tile server able to generate and serve [vector tiles](https://github See [Martin book](https://maplibre.org/martin/) for complete documentation. -![Martin](https://raw.githubusercontent.com/maplibre/martin/main/logo.png) - ## Installation _See [installation instructions](https://maplibre.org/martin/10-installation.html) in the Martin book._ From bad4efc0d44f17a296218c93f62fccf9b6b0fa18 Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Sun, 5 Nov 2023 21:51:33 -0500 Subject: [PATCH 105/108] link to book from the banner --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 70264c034..a0055cab6 100755 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![Martin](https://raw.githubusercontent.com/maplibre/martin/main/logo.png) +[![Martin](https://raw.githubusercontent.com/maplibre/martin/main/logo.png)](https://maplibre.org/martin/) [![Book](https://img.shields.io/badge/docs-Book-informational)](https://maplibre.org/martin) [![docs.rs docs](https://docs.rs/martin/badge.svg)](https://docs.rs/martin) From 687996d27895aa20b6e82e8ba5d6844d1aca3177 Mon Sep 17 00:00:00 2001 From: Angela Chan Date: Mon, 24 Apr 2023 08:43:49 +0200 Subject: [PATCH 106/108] fix: Include Authorization and Accept in CORs allowed headers --- martin/src/srv/server.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/martin/src/srv/server.rs b/martin/src/srv/server.rs index df755853e..f36cf3345 100755 --- a/martin/src/srv/server.rs +++ b/martin/src/srv/server.rs @@ -7,7 +7,7 @@ use actix_web::dev::Server; use actix_web::error::{ErrorBadRequest, ErrorInternalServerError, ErrorNotFound}; use actix_web::http::header::{ AcceptEncoding, ContentType, Encoding as HeaderEnc, HeaderValue, Preference, CACHE_CONTROL, - CONTENT_ENCODING, + CONTENT_ENCODING, AUTHORIZATION, ACCEPT }; use actix_web::http::Uri; use actix_web::middleware::TrailingSlash; @@ -481,7 +481,8 @@ pub fn new_server(config: SrvConfig, state: ServerState) -> crate::Result<(Serve let server = HttpServer::new(move || { let cors_middleware = Cors::default() .allow_any_origin() - .allowed_methods(vec!["GET"]); + .allowed_methods(vec!["GET"]) + .allowed_headers(vec![AUTHORIZATION, ACCEPT]); App::new() .app_data(Data::new(state.tiles.clone())) From 5c990b0278d02f3f86c0308accb48da55426be98 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Oct 2023 18:43:45 +0000 Subject: [PATCH 107/108] chore(deps): Bump rustix from 0.38.14 to 0.38.19 (#14) Bumps [rustix](https://github.com/bytecodealliance/rustix) from 0.38.14 to 0.38.19. - [Release notes](https://github.com/bytecodealliance/rustix/releases) - [Commits](https://github.com/bytecodealliance/rustix/compare/v0.38.14...v0.38.19) --- updated-dependencies: - dependency-name: rustix dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 1 - 1 file changed, 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 71356d944..ce1e06595 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1693,7 +1693,6 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" name = "linux-raw-sys" version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" [[package]] name = "local-channel" From 0871a2d1c24a68ed066df790d7e152b9216d4930 Mon Sep 17 00:00:00 2001 From: jennaramdenee Date: Mon, 6 Nov 2023 17:44:34 +0100 Subject: [PATCH 108/108] Revert "fix: Include Authorization and Accept in CORs allowed headers" This reverts commit ee395a43cca12227aabb16fc5639d71be799fe78. --- martin/src/srv/server.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/martin/src/srv/server.rs b/martin/src/srv/server.rs index f36cf3345..df755853e 100755 --- a/martin/src/srv/server.rs +++ b/martin/src/srv/server.rs @@ -7,7 +7,7 @@ use actix_web::dev::Server; use actix_web::error::{ErrorBadRequest, ErrorInternalServerError, ErrorNotFound}; use actix_web::http::header::{ AcceptEncoding, ContentType, Encoding as HeaderEnc, HeaderValue, Preference, CACHE_CONTROL, - CONTENT_ENCODING, AUTHORIZATION, ACCEPT + CONTENT_ENCODING, }; use actix_web::http::Uri; use actix_web::middleware::TrailingSlash; @@ -481,8 +481,7 @@ pub fn new_server(config: SrvConfig, state: ServerState) -> crate::Result<(Serve let server = HttpServer::new(move || { let cors_middleware = Cors::default() .allow_any_origin() - .allowed_methods(vec!["GET"]) - .allowed_headers(vec![AUTHORIZATION, ACCEPT]); + .allowed_methods(vec!["GET"]); App::new() .app_data(Data::new(state.tiles.clone()))