Add apply-diff command (#747)

Add command `apply-diff` to apply diff file generated from `copy`
command
This commit is contained in:
rstanciu 2023-07-05 14:38:03 -07:00 committed by GitHub
parent e004908722
commit 1342b38e75
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 127 additions and 9 deletions

View File

@ -26,10 +26,13 @@ Copy command can also be used to compare two mbtiles files and generate a diff.
mbtiles copy src_file.mbtiles diff_file.mbtiles --force-simple --diff-with-file modified_file.mbtiles mbtiles copy src_file.mbtiles diff_file.mbtiles --force-simple --diff-with-file modified_file.mbtiles
``` ```
The `diff_file.mbtiles` can be applied to the `src_file.mbtiles` elsewhere to avoid copying/transmitting the entire modified dataset. ### 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.
One way to apply the diff is to use the `sqlite3` command line tool directly. Here, we assume that the `src_file.mbtiles` is in the simple tables format, and that the `diff_file.mbtiles` is the output of the `mbtiles copy` command above. 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`. 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
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`. 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 ```shell
sqlite3 src_file.mbtiles \ sqlite3 src_file.mbtiles \
-bail \ -bail \
@ -38,3 +41,6 @@ sqlite3 src_file.mbtiles \
"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);" \ "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;" "INSERT OR REPLACE INTO tiles (zoom_level, tile_column, tile_row, tile_data) SELECT * FROM diffDb.tiles WHERE tile_data NOTNULL;"
``` ```
**_NOTE:_** Both of these methods for applying a diff _only_ work for mbtiles files in the simple tables format; they do _not_ work for mbtiles files in deduplicated format.

View File

@ -2,7 +2,7 @@ use std::path::{Path, PathBuf};
use anyhow::Result; use anyhow::Result;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use martin_mbtiles::{copy_mbtiles_file, Mbtiles, TileCopierOptions}; use martin_mbtiles::{apply_mbtiles_diff, copy_mbtiles_file, Mbtiles, TileCopierOptions};
use sqlx::sqlite::SqliteConnectOptions; use sqlx::sqlite::SqliteConnectOptions;
use sqlx::{Connection, SqliteConnection}; use sqlx::{Connection, SqliteConnection};
@ -45,6 +45,14 @@ enum Commands {
/// Copy tiles from one mbtiles file to another. /// Copy tiles from one mbtiles file to another.
#[command(name = "copy")] #[command(name = "copy")]
Copy(TileCopierOptions), Copy(TileCopierOptions),
/// Apply diff file generated from 'copy' command
#[command(name = "apply-diff")]
ApplyDiff {
/// MBTiles file to apply diff to
src_file: PathBuf,
/// Diff file
diff_file: PathBuf,
},
} }
#[tokio::main] #[tokio::main]
@ -58,6 +66,12 @@ async fn main() -> Result<()> {
Commands::Copy(opts) => { Commands::Copy(opts) => {
copy_mbtiles_file(opts).await?; copy_mbtiles_file(opts).await?;
} }
Commands::ApplyDiff {
src_file,
diff_file,
} => {
apply_mbtiles_diff(src_file, diff_file).await?;
}
} }
Ok(()) Ok(())
@ -82,7 +96,7 @@ mod tests {
use martin_mbtiles::TileCopierOptions; use martin_mbtiles::TileCopierOptions;
use crate::Args; use crate::Args;
use crate::Commands::{Copy, MetaGetValue}; use crate::Commands::{ApplyDiff, Copy, MetaGetValue};
#[test] #[test]
fn test_copy_no_arguments() { fn test_copy_no_arguments() {
@ -254,4 +268,18 @@ mod tests {
} }
); );
} }
#[test]
fn test_apply_diff_with_arguments() {
assert_eq!(
Args::parse_from(["mbtiles", "apply-diff", "src_file", "diff_file"]),
Args {
verbose: false,
command: ApplyDiff {
src_file: PathBuf::from("src_file"),
diff_file: PathBuf::from("diff_file"),
}
}
);
}
} }

View File

@ -1,5 +1,6 @@
use std::path::PathBuf; use std::path::PathBuf;
use crate::mbtiles::MbtType;
use martin_tile_utils::TileInfo; use martin_tile_utils::TileInfo;
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
@ -16,6 +17,9 @@ pub enum MbtError {
#[error("Invalid data format for MBTile file {0}")] #[error("Invalid data format for MBTile file {0}")]
InvalidDataFormat(String), InvalidDataFormat(String),
#[error("Incorrect data format for MBTile file {0}; expected {1:?} and got {2:?}")]
IncorrectDataFormat(String, MbtType, MbtType),
#[error(r#"Filename "{0}" passed to SQLite must be valid UTF-8"#)] #[error(r#"Filename "{0}" passed to SQLite must be valid UTF-8"#)]
InvalidFilenameType(PathBuf), InvalidFilenameType(PathBuf),

View File

@ -9,4 +9,4 @@ mod tile_copier;
pub use errors::MbtError; pub use errors::MbtError;
pub use mbtiles::{Mbtiles, Metadata}; pub use mbtiles::{Mbtiles, Metadata};
pub use mbtiles_pool::MbtilesPool; pub use mbtiles_pool::MbtilesPool;
pub use tile_copier::{copy_mbtiles_file, TileCopierOptions}; pub use tile_copier::{apply_mbtiles_diff, copy_mbtiles_file, TileCopierOptions};

View File

@ -10,6 +10,7 @@ use sqlx::{query, query_with, Arguments, Connection, Row, SqliteConnection};
use crate::errors::MbtResult; use crate::errors::MbtResult;
use crate::mbtiles::MbtType; use crate::mbtiles::MbtType;
use crate::mbtiles::MbtType::TileTables;
use crate::{MbtError, Mbtiles}; use crate::{MbtError, Mbtiles};
#[derive(Clone, Default, PartialEq, Eq, Debug)] #[derive(Clone, Default, PartialEq, Eq, Debug)]
@ -290,6 +291,47 @@ async fn open_and_detect_type(mbtiles: &Mbtiles) -> MbtResult<MbtType> {
mbtiles.detect_type(&mut conn).await mbtiles.detect_type(&mut conn).await
} }
pub async fn apply_mbtiles_diff(
src_file: PathBuf,
diff_file: PathBuf,
) -> MbtResult<SqliteConnection> {
let src_mbtiles = Mbtiles::new(src_file)?;
let diff_mbtiles = Mbtiles::new(diff_file)?;
let opt = SqliteConnectOptions::new().filename(src_mbtiles.filepath());
let mut conn = SqliteConnection::connect_with(&opt).await?;
let src_type = src_mbtiles.detect_type(&mut conn).await?;
if src_type != MbtType::TileTables {
return Err(MbtError::IncorrectDataFormat(
src_mbtiles.filepath().to_string(),
TileTables,
src_type,
));
}
open_and_detect_type(&diff_mbtiles).await?;
query("ATTACH DATABASE ? AS diffDb")
.bind(diff_mbtiles.filepath())
.execute(&mut conn)
.await?;
query(
"
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;",
)
.execute(&mut conn)
.await?;
Ok(conn)
}
pub async fn copy_mbtiles_file(opts: TileCopierOptions) -> MbtResult<SqliteConnection> { pub async fn copy_mbtiles_file(opts: TileCopierOptions) -> MbtResult<SqliteConnection> {
let tile_copier = TileCopier::new(opts)?; let tile_copier = TileCopier::new(opts)?;
@ -464,4 +506,34 @@ mod tests {
.await .await
.is_none()); .is_none());
} }
#[actix_rt::test]
async fn apply_diff_file() {
// 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::memory:?cache=shared");
let _src_conn = copy_mbtiles_file(TileCopierOptions::new(src_file.clone(), src.clone()))
.await
.unwrap();
// Apply diff to the src data in in-memory DB
let diff_file = PathBuf::from("../tests/fixtures/files/world_cities_diff.mbtiles");
let mut src_conn = apply_mbtiles_diff(src, diff_file).await.unwrap();
// Verify the data is the same as the file the diff was generated from
let modified_file = PathBuf::from("../tests/fixtures/files/world_cities_modified.mbtiles");
let _ = query("ATTACH DATABASE ? AS otherDb")
.bind(modified_file.clone().to_str().unwrap())
.execute(&mut src_conn)
.await;
assert!(
query("SELECT * FROM tiles EXCEPT SELECT * FROM otherDb.tiles;")
.fetch_optional(&mut src_conn)
.await
.unwrap()
.is_none()
);
}
} }

View File

@ -165,6 +165,12 @@
"name": "Major cities from Natural Earth data", "name": "Major cities from Natural Earth data",
"description": "Major cities from Natural Earth data" "description": "Major cities from Natural Earth data"
}, },
{
"id": "world_cities_diff",
"content_type": "application/x-protobuf",
"name": "Major cities from Natural Earth data",
"description": "Major cities from Natural Earth data"
},
{ {
"id": "world_cities_modified", "id": "world_cities_modified",
"content_type": "application/x-protobuf", "content_type": "application/x-protobuf",

View File

@ -158,4 +158,5 @@ mbtiles:
uncompressed_mvt: tests/fixtures/files/uncompressed_mvt.mbtiles uncompressed_mvt: tests/fixtures/files/uncompressed_mvt.mbtiles
webp: tests/fixtures/files/webp.mbtiles webp: tests/fixtures/files/webp.mbtiles
world_cities: tests/fixtures/files/world_cities.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 world_cities_modified: tests/fixtures/files/world_cities_modified.mbtiles

View File

@ -5,6 +5,7 @@ Usage: mbtiles <COMMAND>
Commands: Commands:
meta-get Gets a single value from the MBTiles metadata table meta-get Gets a single value from the MBTiles metadata table
copy Copy tiles from one mbtiles file to another copy Copy tiles from one mbtiles file to another
apply-diff Apply diff file generated from 'copy' command
help Print this message or the help of the given subcommand(s) help Print this message or the help of the given subcommand(s)
Options: Options:

Binary file not shown.