mirror of
https://github.com/maplibre/martin.git
synced 2024-12-24 07:22:52 +03:00
Add agg_tiles_hash_before_apply
, warnings, and validate on patch (#1266)
Implement #1244
This commit is contained in:
parent
d8defffdda
commit
6320e0fff3
@ -66,6 +66,9 @@ enum Commands {
|
|||||||
base_file: PathBuf,
|
base_file: PathBuf,
|
||||||
/// Diff file
|
/// Diff file
|
||||||
patch_file: PathBuf,
|
patch_file: PathBuf,
|
||||||
|
/// Force patching operation, ignoring some warnings that otherwise would prevent the operation. Use with caution.
|
||||||
|
#[arg(short, long)]
|
||||||
|
force: bool,
|
||||||
},
|
},
|
||||||
/// Update metadata to match the content of the file
|
/// Update metadata to match the content of the file
|
||||||
#[command(name = "meta-update", alias = "update-meta")]
|
#[command(name = "meta-update", alias = "update-meta")]
|
||||||
@ -156,6 +159,12 @@ pub struct SharedCopyOpts {
|
|||||||
/// Skip generating a global hash for mbtiles validation. By default, `mbtiles` will compute `agg_tiles_hash` metadata value.
|
/// Skip generating a global hash for mbtiles validation. By default, `mbtiles` will compute `agg_tiles_hash` metadata value.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
skip_agg_tiles_hash: bool,
|
skip_agg_tiles_hash: bool,
|
||||||
|
/// Force copy operation, ignoring some warnings that otherwise would prevent the operation. Use with caution.
|
||||||
|
#[arg(short, long)]
|
||||||
|
force: bool,
|
||||||
|
/// Perform agg_hash validation on the original and destination files.
|
||||||
|
#[arg(long)]
|
||||||
|
validate: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SharedCopyOpts {
|
impl SharedCopyOpts {
|
||||||
@ -181,6 +190,8 @@ impl SharedCopyOpts {
|
|||||||
zoom_levels: self.zoom_levels,
|
zoom_levels: self.zoom_levels,
|
||||||
bbox: self.bbox,
|
bbox: self.bbox,
|
||||||
skip_agg_tiles_hash: self.skip_agg_tiles_hash,
|
skip_agg_tiles_hash: self.skip_agg_tiles_hash,
|
||||||
|
force: self.force,
|
||||||
|
validate: self.validate,
|
||||||
// Constants
|
// Constants
|
||||||
dst_type: None, // Taken from dst_type_cli
|
dst_type: None, // Taken from dst_type_cli
|
||||||
}
|
}
|
||||||
@ -233,8 +244,9 @@ async fn main_int() -> anyhow::Result<()> {
|
|||||||
Commands::ApplyPatch {
|
Commands::ApplyPatch {
|
||||||
base_file,
|
base_file,
|
||||||
patch_file,
|
patch_file,
|
||||||
|
force,
|
||||||
} => {
|
} => {
|
||||||
apply_patch(base_file, patch_file).await?;
|
apply_patch(base_file, patch_file, force).await?;
|
||||||
}
|
}
|
||||||
Commands::UpdateMetadata { file, update_zoom } => {
|
Commands::UpdateMetadata { file, update_zoom } => {
|
||||||
let mbt = Mbtiles::new(file.as_path())?;
|
let mbt = Mbtiles::new(file.as_path())?;
|
||||||
@ -258,7 +270,7 @@ async fn main_int() -> anyhow::Result<()> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
let mbt = Mbtiles::new(file.as_path())?;
|
let mbt = Mbtiles::new(file.as_path())?;
|
||||||
mbt.validate(integrity_check, agg_hash).await?;
|
mbt.open_and_validate(integrity_check, agg_hash).await?;
|
||||||
}
|
}
|
||||||
Commands::Summary { file } => {
|
Commands::Summary { file } => {
|
||||||
let mbt = Mbtiles::new(file.as_path())?;
|
let mbt = Mbtiles::new(file.as_path())?;
|
||||||
@ -597,6 +609,7 @@ mod tests {
|
|||||||
command: ApplyPatch {
|
command: ApplyPatch {
|
||||||
base_file: PathBuf::from("src_file"),
|
base_file: PathBuf::from("src_file"),
|
||||||
patch_file: PathBuf::from("diff_file"),
|
patch_file: PathBuf::from("diff_file"),
|
||||||
|
force: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -3,21 +3,24 @@ use std::path::PathBuf;
|
|||||||
|
|
||||||
use enum_display::EnumDisplay;
|
use enum_display::EnumDisplay;
|
||||||
use itertools::Itertools as _;
|
use itertools::Itertools as _;
|
||||||
use log::{debug, info, trace};
|
use log::{debug, info, trace, warn};
|
||||||
use martin_tile_utils::{bbox_to_xyz, MAX_ZOOM};
|
use martin_tile_utils::{bbox_to_xyz, MAX_ZOOM};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlite_hashes::rusqlite::Connection;
|
use sqlite_hashes::rusqlite::Connection;
|
||||||
use sqlx::{query, Executor as _, Row, SqliteConnection};
|
use sqlx::{query, Connection as _, Executor as _, Row, SqliteConnection};
|
||||||
use tilejson::Bounds;
|
use tilejson::Bounds;
|
||||||
|
|
||||||
use crate::errors::MbtResult;
|
use crate::errors::MbtResult;
|
||||||
|
use crate::mbtiles::PatchFileInfo;
|
||||||
use crate::queries::{
|
use crate::queries::{
|
||||||
create_tiles_with_hash_view, detach_db, init_mbtiles_schema, is_empty_database,
|
create_tiles_with_hash_view, detach_db, init_mbtiles_schema, is_empty_database,
|
||||||
};
|
};
|
||||||
|
use crate::AggHashType::Verify;
|
||||||
|
use crate::IntegrityCheckType::Quick;
|
||||||
use crate::MbtType::{Flat, FlatWithHash, Normalized};
|
use crate::MbtType::{Flat, FlatWithHash, Normalized};
|
||||||
use crate::{
|
use crate::{
|
||||||
invert_y_value, reset_db_settings, CopyType, MbtError, MbtType, MbtTypeCli, Mbtiles,
|
invert_y_value, reset_db_settings, CopyType, MbtError, MbtType, MbtTypeCli, Mbtiles,
|
||||||
AGG_TILES_HASH, AGG_TILES_HASH_AFTER_APPLY,
|
AGG_TILES_HASH, AGG_TILES_HASH_AFTER_APPLY, AGG_TILES_HASH_BEFORE_APPLY,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, EnumDisplay)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, EnumDisplay)]
|
||||||
@ -68,12 +71,16 @@ pub struct MbtilesCopier {
|
|||||||
pub apply_patch: Option<PathBuf>,
|
pub apply_patch: Option<PathBuf>,
|
||||||
/// Skip generating a global hash for mbtiles validation. By default, `mbtiles` will compute `agg_tiles_hash` metadata value.
|
/// Skip generating a global hash for mbtiles validation. By default, `mbtiles` will compute `agg_tiles_hash` metadata value.
|
||||||
pub skip_agg_tiles_hash: bool,
|
pub skip_agg_tiles_hash: bool,
|
||||||
|
/// Ignore some warnings and continue with the copying operation
|
||||||
|
pub force: bool,
|
||||||
|
/// Perform `agg_hash` validation on the original and destination files.
|
||||||
|
pub validate: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
struct MbtileCopierInt {
|
struct MbtileCopierInt {
|
||||||
src_mbtiles: Mbtiles,
|
src_mbt: Mbtiles,
|
||||||
dst_mbtiles: Mbtiles,
|
dst_mbt: Mbtiles,
|
||||||
options: MbtilesCopier,
|
options: MbtilesCopier,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,23 +121,30 @@ impl MbtileCopierInt {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Ok(MbtileCopierInt {
|
Ok(MbtileCopierInt {
|
||||||
src_mbtiles: Mbtiles::new(&options.src_file)?,
|
src_mbt: Mbtiles::new(&options.src_file)?,
|
||||||
dst_mbtiles: Mbtiles::new(&options.dst_file)?,
|
dst_mbt: Mbtiles::new(&options.dst_file)?,
|
||||||
options,
|
options,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run(self) -> MbtResult<SqliteConnection> {
|
pub async fn run(self) -> MbtResult<SqliteConnection> {
|
||||||
if self.options.diff_with_file.is_none() && self.options.apply_patch.is_none() {
|
if let Some(diff_file) = &self.options.diff_with_file {
|
||||||
self.run_simple().await
|
let mbt = Mbtiles::new(diff_file)?;
|
||||||
|
self.run_with_diff(mbt).await
|
||||||
|
} else if let Some(patch_file) = &self.options.apply_patch {
|
||||||
|
let mbt = Mbtiles::new(patch_file)?;
|
||||||
|
self.run_with_patch(mbt).await
|
||||||
} else {
|
} else {
|
||||||
self.run_with_diff_or_patch().await
|
self.run_simple().await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run_simple(self) -> MbtResult<SqliteConnection> {
|
async fn run_simple(self) -> MbtResult<SqliteConnection> {
|
||||||
let src_type = self.src_mbtiles.open_and_detect_type().await?;
|
let mut conn = self.src_mbt.open_readonly().await?;
|
||||||
let mut conn = self.dst_mbtiles.open_or_new().await?;
|
let src_type = self.src_mbt.detect_type(&mut conn).await?;
|
||||||
|
conn.close().await?;
|
||||||
|
|
||||||
|
conn = self.dst_mbt.open_or_new().await?;
|
||||||
let is_empty_db = is_empty_database(&mut conn).await?;
|
let is_empty_db = is_empty_database(&mut conn).await?;
|
||||||
|
|
||||||
let on_duplicate = if let Some(on_duplicate) = self.options.on_duplicate {
|
let on_duplicate = if let Some(on_duplicate) = self.options.on_duplicate {
|
||||||
@ -141,24 +155,24 @@ impl MbtileCopierInt {
|
|||||||
return Err(MbtError::DestinationFileExists(self.options.dst_file));
|
return Err(MbtError::DestinationFileExists(self.options.dst_file));
|
||||||
};
|
};
|
||||||
|
|
||||||
self.src_mbtiles.attach_to(&mut conn, "sourceDb").await?;
|
self.src_mbt.attach_to(&mut conn, "sourceDb").await?;
|
||||||
|
|
||||||
let dst_type = if is_empty_db {
|
let dst_type = if is_empty_db {
|
||||||
self.options.dst_type().unwrap_or(src_type)
|
self.options.dst_type().unwrap_or(src_type)
|
||||||
} else {
|
} else {
|
||||||
self.validate_dst_type(self.dst_mbtiles.detect_type(&mut conn).await?)?
|
self.validate_dst_type(self.dst_mbt.detect_type(&mut conn).await?)?
|
||||||
};
|
};
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
"Copying {src_mbt} ({src_type}) {what}to a {is_new} file {dst_mbt} ({dst_type})",
|
"Copying {src_mbt} ({src_type}) {what}to a {is_new} file {dst_mbt} ({dst_type})",
|
||||||
src_mbt = self.src_mbtiles,
|
src_mbt = self.src_mbt,
|
||||||
what = self.copy_text(),
|
what = self.copy_text(),
|
||||||
is_new = if is_empty_db { "new" } else { "existing" },
|
is_new = if is_empty_db { "new" } else { "existing" },
|
||||||
dst_mbt = self.dst_mbtiles,
|
dst_mbt = self.dst_mbt,
|
||||||
);
|
);
|
||||||
|
|
||||||
if is_empty_db {
|
if is_empty_db {
|
||||||
self.init_new_schema(&mut conn, src_type, dst_type).await?;
|
self.init_schema(&mut conn, src_type, dst_type).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.copy_with_rusqlite(
|
self.copy_with_rusqlite(
|
||||||
@ -166,12 +180,11 @@ impl MbtileCopierInt {
|
|||||||
on_duplicate,
|
on_duplicate,
|
||||||
dst_type,
|
dst_type,
|
||||||
get_select_from(src_type, dst_type),
|
get_select_from(src_type, dst_type),
|
||||||
false,
|
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if self.options.copy.copy_tiles() && !self.options.skip_agg_tiles_hash {
|
if self.options.copy.copy_tiles() && !self.options.skip_agg_tiles_hash {
|
||||||
self.dst_mbtiles.update_agg_tiles_hash(&mut conn).await?;
|
self.dst_mbt.update_agg_tiles_hash(&mut conn).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
detach_db(&mut conn, "sourceDb").await?;
|
detach_db(&mut conn, "sourceDb").await?;
|
||||||
@ -179,62 +192,149 @@ impl MbtileCopierInt {
|
|||||||
Ok(conn)
|
Ok(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run_with_diff_or_patch(self) -> MbtResult<SqliteConnection> {
|
/// Compare two files, and write their difference to the diff file
|
||||||
let ((Some(dif_file), None) | (None, Some(dif_file))) =
|
async fn run_with_diff(self, dif_mbt: Mbtiles) -> MbtResult<SqliteConnection> {
|
||||||
(&self.options.diff_with_file, &self.options.apply_patch)
|
let mut dif_conn = dif_mbt.open_readonly().await?;
|
||||||
else {
|
let dif_info = dif_mbt.examine_diff(&mut dif_conn).await?;
|
||||||
unreachable!()
|
dif_mbt.assert_hashes(&dif_info, self.options.force)?;
|
||||||
};
|
dif_conn.close().await?;
|
||||||
let dif_mbt = Mbtiles::new(dif_file)?;
|
|
||||||
let dif_type = dif_mbt.open_and_detect_type().await?;
|
|
||||||
let is_creating_diff = self.options.diff_with_file.is_some();
|
|
||||||
|
|
||||||
let src_type = self.src_mbtiles.open_and_detect_type().await?;
|
let src_info = self.validate_src_file().await?;
|
||||||
|
|
||||||
let mut conn = self.dst_mbtiles.open_or_new().await?;
|
let mut conn = self.dst_mbt.open_or_new().await?;
|
||||||
if !is_empty_database(&mut conn).await? {
|
if !is_empty_database(&mut conn).await? {
|
||||||
return Err(MbtError::NonEmptyTargetFile(self.options.dst_file));
|
return Err(MbtError::NonEmptyTargetFile(self.options.dst_file));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.src_mbtiles.attach_to(&mut conn, "sourceDb").await?;
|
self.src_mbt.attach_to(&mut conn, "sourceDb").await?;
|
||||||
dif_mbt.attach_to(&mut conn, "diffDb").await?;
|
dif_mbt.attach_to(&mut conn, "diffDb").await?;
|
||||||
|
|
||||||
let what = self.copy_text();
|
let dst_type = self.options.dst_type().unwrap_or(src_info.mbt_type);
|
||||||
let src_path = &self.src_mbtiles.filepath();
|
info!(
|
||||||
let dst_path = &self.dst_mbtiles.filepath();
|
"Comparing {src_mbt} ({src_type}) and {dif_path} ({dif_type}) {what}into a new file {dst_path} ({dst_type})",
|
||||||
let dif_path = dif_mbt.filepath();
|
src_mbt = self.src_mbt,
|
||||||
let dst_type = self.options.dst_type().unwrap_or(src_type);
|
src_type = src_info.mbt_type,
|
||||||
if is_creating_diff {
|
dif_path = dif_mbt.filepath(),
|
||||||
info!("Comparing {src_path} ({src_type}) and {dif_path} ({dif_type}) {what}into a new file {dst_path} ({dst_type})");
|
dif_type = dif_info.mbt_type,
|
||||||
} else {
|
what = self.copy_text(),
|
||||||
info!("Applying patch from {dif_path} ({dif_type}) to {src_path} ({src_type}) {what}into a new file {dst_path} ({dst_type})");
|
dst_path = self.dst_mbt.filepath()
|
||||||
}
|
);
|
||||||
|
|
||||||
self.init_new_schema(&mut conn, src_type, dst_type).await?;
|
|
||||||
|
|
||||||
|
self.init_schema(&mut conn, src_info.mbt_type, dst_type)
|
||||||
|
.await?;
|
||||||
self.copy_with_rusqlite(
|
self.copy_with_rusqlite(
|
||||||
&mut conn,
|
&mut conn,
|
||||||
CopyDuplicateMode::Override,
|
CopyDuplicateMode::Override,
|
||||||
dst_type,
|
dst_type,
|
||||||
&(if is_creating_diff {
|
&get_select_from_with_diff(dif_info.mbt_type, dst_type),
|
||||||
get_select_from_with_diff(dif_type, dst_type)
|
|
||||||
} else {
|
|
||||||
get_select_from_apply_patch(src_type, dif_type, dst_type)
|
|
||||||
}),
|
|
||||||
true,
|
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
if let Some(hash) = src_info.agg_tiles_hash {
|
||||||
|
self.dst_mbt
|
||||||
|
.set_metadata_value(&mut conn, AGG_TILES_HASH_BEFORE_APPLY, &hash)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
if let Some(hash) = dif_info.agg_tiles_hash {
|
||||||
|
self.dst_mbt
|
||||||
|
.set_metadata_value(&mut conn, AGG_TILES_HASH_AFTER_APPLY, &hash)
|
||||||
|
.await?;
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: perhaps disable all except --copy all when using with diffs, or else is not making much sense
|
||||||
if self.options.copy.copy_tiles() && !self.options.skip_agg_tiles_hash {
|
if self.options.copy.copy_tiles() && !self.options.skip_agg_tiles_hash {
|
||||||
self.dst_mbtiles.update_agg_tiles_hash(&mut conn).await?;
|
self.dst_mbt.update_agg_tiles_hash(&mut conn).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
detach_db(&mut conn, "diffDb").await?;
|
||||||
|
detach_db(&mut conn, "sourceDb").await?;
|
||||||
|
self.validate(&self.dst_mbt, &mut conn).await?;
|
||||||
|
|
||||||
|
Ok(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply a patch file to the source file and write the result to the destination file
|
||||||
|
async fn run_with_patch(self, dif_mbt: Mbtiles) -> MbtResult<SqliteConnection> {
|
||||||
|
let mut dif_conn = dif_mbt.open_readonly().await?;
|
||||||
|
let dif_info = dif_mbt.examine_diff(&mut dif_conn).await?;
|
||||||
|
self.validate(&dif_mbt, &mut dif_conn).await?;
|
||||||
|
dif_mbt.validate_diff_info(&dif_info, self.options.force)?;
|
||||||
|
dif_conn.close().await?;
|
||||||
|
|
||||||
|
let src_type = self.validate_src_file().await?.mbt_type;
|
||||||
|
|
||||||
|
let mut conn = self.dst_mbt.open_or_new().await?;
|
||||||
|
if !is_empty_database(&mut conn).await? {
|
||||||
|
return Err(MbtError::NonEmptyTargetFile(self.options.dst_file));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.src_mbt.attach_to(&mut conn, "sourceDb").await?;
|
||||||
|
dif_mbt.attach_to(&mut conn, "diffDb").await?;
|
||||||
|
|
||||||
|
let dst_type = self.options.dst_type().unwrap_or(src_type);
|
||||||
|
info!("Applying patch from {dif_path} ({dif_type}) to {src_mbt} ({src_type}) {what}into a new file {dst_path} ({dst_type})",
|
||||||
|
dif_path = dif_mbt.filepath(),
|
||||||
|
dif_type = dif_info.mbt_type,
|
||||||
|
src_mbt = self.src_mbt,
|
||||||
|
what = self.copy_text(),
|
||||||
|
dst_path = self.dst_mbt.filepath());
|
||||||
|
|
||||||
|
self.init_schema(&mut conn, src_type, dst_type).await?;
|
||||||
|
self.copy_with_rusqlite(
|
||||||
|
&mut conn,
|
||||||
|
CopyDuplicateMode::Override,
|
||||||
|
dst_type,
|
||||||
|
&get_select_from_apply_patch(src_type, dif_info.mbt_type, dst_type),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// TODO: perhaps disable all except --copy all when using with diffs, or else is not making much sense
|
||||||
|
if self.options.copy.copy_tiles() && !self.options.skip_agg_tiles_hash {
|
||||||
|
self.dst_mbt.update_agg_tiles_hash(&mut conn).await?;
|
||||||
|
|
||||||
|
let new_hash = self.dst_mbt.get_agg_tiles_hash(&mut conn).await?;
|
||||||
|
match (dif_info.agg_tiles_hash_after_apply, new_hash) {
|
||||||
|
(Some(expected), Some(actual)) if expected != actual => {
|
||||||
|
let err = MbtError::AggHashMismatchAfterApply(
|
||||||
|
dif_mbt.filepath().to_string(),
|
||||||
|
expected,
|
||||||
|
self.dst_mbt.filepath().to_string(),
|
||||||
|
actual,
|
||||||
|
);
|
||||||
|
if !self.options.force {
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
warn!("{err}");
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
detach_db(&mut conn, "diffDb").await?;
|
detach_db(&mut conn, "diffDb").await?;
|
||||||
detach_db(&mut conn, "sourceDb").await?;
|
detach_db(&mut conn, "sourceDb").await?;
|
||||||
|
|
||||||
|
self.validate(&self.dst_mbt, &mut conn).await?;
|
||||||
|
|
||||||
Ok(conn)
|
Ok(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn validate(&self, mbt: &Mbtiles, conn: &mut SqliteConnection) -> MbtResult<()> {
|
||||||
|
if self.options.validate {
|
||||||
|
mbt.validate(conn, Quick, Verify).await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn validate_src_file(&self) -> MbtResult<PatchFileInfo> {
|
||||||
|
let mut src_conn = self.src_mbt.open_readonly().await?;
|
||||||
|
let src_info = self.src_mbt.examine_diff(&mut src_conn).await?;
|
||||||
|
self.validate(&self.src_mbt, &mut src_conn).await?;
|
||||||
|
self.src_mbt.assert_hashes(&src_info, self.options.force)?;
|
||||||
|
src_conn.close().await?;
|
||||||
|
|
||||||
|
Ok(src_info)
|
||||||
|
}
|
||||||
|
|
||||||
fn copy_text(&self) -> &str {
|
fn copy_text(&self) -> &str {
|
||||||
match self.options.copy {
|
match self.options.copy {
|
||||||
CopyType::All => "",
|
CopyType::All => "",
|
||||||
@ -249,7 +349,6 @@ impl MbtileCopierInt {
|
|||||||
on_duplicate: CopyDuplicateMode,
|
on_duplicate: CopyDuplicateMode,
|
||||||
dst_type: MbtType,
|
dst_type: MbtType,
|
||||||
select_from: &str,
|
select_from: &str,
|
||||||
is_diff: bool,
|
|
||||||
) -> Result<(), MbtError> {
|
) -> Result<(), MbtError> {
|
||||||
// SAFETY: This must be scoped to make sure the handle is dropped before we continue using conn
|
// SAFETY: This must be scoped to make sure the handle is dropped before we continue using conn
|
||||||
// Make sure not to execute any other queries while the handle is locked
|
// Make sure not to execute any other queries while the handle is locked
|
||||||
@ -266,7 +365,7 @@ impl MbtileCopierInt {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if self.options.copy.copy_metadata() {
|
if self.options.copy.copy_metadata() {
|
||||||
self.copy_metadata(&rusqlite_conn, is_diff, on_duplicate)
|
self.copy_metadata(&rusqlite_conn, on_duplicate)
|
||||||
} else {
|
} else {
|
||||||
debug!("Skipping copying metadata");
|
debug!("Skipping copying metadata");
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -276,38 +375,35 @@ impl MbtileCopierInt {
|
|||||||
fn copy_metadata(
|
fn copy_metadata(
|
||||||
&self,
|
&self,
|
||||||
rusqlite_conn: &Connection,
|
rusqlite_conn: &Connection,
|
||||||
is_diff: bool,
|
|
||||||
on_duplicate: CopyDuplicateMode,
|
on_duplicate: CopyDuplicateMode,
|
||||||
) -> Result<(), MbtError> {
|
) -> Result<(), MbtError> {
|
||||||
let on_dupl = on_duplicate.to_sql();
|
let on_dupl = on_duplicate.to_sql();
|
||||||
let sql;
|
let sql;
|
||||||
if is_diff {
|
|
||||||
// Insert all rows from diffDb.metadata if they do not exist or are different in sourceDb.metadata.
|
// 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.
|
// 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_after_apply because agg_tiles_hash will be auto-added later
|
// Skip agg_tiles_hash because that requires special handling
|
||||||
if self.options.diff_with_file.is_some() {
|
if self.options.diff_with_file.is_some() {
|
||||||
// Include agg_tiles_hash value even if it is the same because we will still need it when applying the diff
|
// Include agg_tiles_hash value even if it is the same because we will still need it when applying the diff
|
||||||
sql = format!(
|
sql = format!(
|
||||||
"
|
"
|
||||||
INSERT {on_dupl} INTO metadata (name, value)
|
INSERT {on_dupl} INTO metadata (name, value)
|
||||||
SELECT IIF(name = '{AGG_TILES_HASH}','{AGG_TILES_HASH_AFTER_APPLY}', name) as name
|
SELECT name, value
|
||||||
, value
|
|
||||||
FROM (
|
FROM (
|
||||||
SELECT COALESCE(difMD.name, srcMD.name) as name
|
SELECT COALESCE(difMD.name, srcMD.name) as name
|
||||||
, difMD.value as value
|
, difMD.value as value
|
||||||
FROM sourceDb.metadata AS srcMD FULL JOIN diffDb.metadata AS difMD
|
FROM sourceDb.metadata AS srcMD FULL JOIN diffDb.metadata AS difMD
|
||||||
ON srcMD.name = difMD.name
|
ON srcMD.name = difMD.name
|
||||||
WHERE srcMD.value != difMD.value OR srcMD.value ISNULL OR difMD.value ISNULL OR srcMD.name = '{AGG_TILES_HASH}'
|
WHERE srcMD.value != difMD.value OR srcMD.value ISNULL OR difMD.value ISNULL
|
||||||
) joinedMD
|
) joinedMD
|
||||||
WHERE name != '{AGG_TILES_HASH_AFTER_APPLY}'"
|
WHERE name NOT IN ('{AGG_TILES_HASH}', '{AGG_TILES_HASH_BEFORE_APPLY}', '{AGG_TILES_HASH_AFTER_APPLY}')"
|
||||||
);
|
);
|
||||||
debug!("Copying metadata, taking into account diff file with {sql}");
|
debug!("Copying metadata, taking into account diff file with {sql}");
|
||||||
} else {
|
} else if self.options.apply_patch.is_some() {
|
||||||
sql = format!(
|
sql = format!(
|
||||||
"
|
"
|
||||||
INSERT {on_dupl} INTO metadata (name, value)
|
INSERT {on_dupl} INTO metadata (name, value)
|
||||||
SELECT IIF(name = '{AGG_TILES_HASH_AFTER_APPLY}','{AGG_TILES_HASH}', name) as name
|
SELECT name, value
|
||||||
, value
|
|
||||||
FROM (
|
FROM (
|
||||||
SELECT COALESCE(srcMD.name, difMD.name) as name
|
SELECT COALESCE(srcMD.name, difMD.name) as name
|
||||||
, COALESCE(difMD.value, srcMD.value) as value
|
, COALESCE(difMD.value, srcMD.value) as value
|
||||||
@ -315,10 +411,9 @@ impl MbtileCopierInt {
|
|||||||
ON srcMD.name = difMD.name
|
ON srcMD.name = difMD.name
|
||||||
WHERE difMD.name ISNULL OR difMD.value NOTNULL
|
WHERE difMD.name ISNULL OR difMD.value NOTNULL
|
||||||
) joinedMD
|
) joinedMD
|
||||||
WHERE name != '{AGG_TILES_HASH}'"
|
WHERE name NOT IN ('{AGG_TILES_HASH}', '{AGG_TILES_HASH_BEFORE_APPLY}', '{AGG_TILES_HASH_AFTER_APPLY}')"
|
||||||
);
|
);
|
||||||
debug!("Copying metadata, and applying the diff file with {sql}");
|
debug!("Copying metadata, and applying the diff file with {sql}");
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
sql = format!(
|
sql = format!(
|
||||||
"
|
"
|
||||||
@ -404,7 +499,7 @@ impl MbtileCopierInt {
|
|||||||
Ok(dst_type)
|
Ok(dst_type)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn init_new_schema(
|
async fn init_schema(
|
||||||
&self,
|
&self,
|
||||||
conn: &mut SqliteConnection,
|
conn: &mut SqliteConnection,
|
||||||
src: MbtType,
|
src: MbtType,
|
||||||
@ -826,6 +921,7 @@ mod tests {
|
|||||||
src_file: src.clone(),
|
src_file: src.clone(),
|
||||||
dst_file: dst.clone(),
|
dst_file: dst.clone(),
|
||||||
diff_with_file: Some(diff_file.clone()),
|
diff_with_file: Some(diff_file.clone()),
|
||||||
|
force: true,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let mut dst_conn = opt.run().await?;
|
let mut dst_conn = opt.run().await?;
|
||||||
|
@ -3,7 +3,7 @@ use std::path::PathBuf;
|
|||||||
use martin_tile_utils::{TileInfo, MAX_ZOOM};
|
use martin_tile_utils::{TileInfo, MAX_ZOOM};
|
||||||
use sqlite_hashes::rusqlite;
|
use sqlite_hashes::rusqlite;
|
||||||
|
|
||||||
use crate::MbtType;
|
use crate::{MbtType, AGG_TILES_HASH, AGG_TILES_HASH_AFTER_APPLY, AGG_TILES_HASH_BEFORE_APPLY};
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub enum MbtError {
|
pub enum MbtError {
|
||||||
@ -77,6 +77,24 @@ pub enum MbtError {
|
|||||||
|
|
||||||
#[error("Invalid zoom value {0}={1}, expecting an integer between 0..{MAX_ZOOM}")]
|
#[error("Invalid zoom value {0}={1}, expecting an integer between 0..{MAX_ZOOM}")]
|
||||||
InvalidZoomValue(&'static str, String),
|
InvalidZoomValue(&'static str, String),
|
||||||
|
|
||||||
|
#[error("A file {0} does not have an {AGG_TILES_HASH} metadata entry, probably because it was not created by this tool. Use `--force` to ignore this warning, or run this to update hash value: `mbtiles validate --agg-hash update {0}`")]
|
||||||
|
CannotDiffFileWithoutHash(String),
|
||||||
|
|
||||||
|
#[error("File {0} has {AGG_TILES_HASH_BEFORE_APPLY} or {AGG_TILES_HASH_AFTER_APPLY} metadata entry, indicating it is a patch file which should not be diffed with another file. Use `--force` to ignore this warning.")]
|
||||||
|
DiffingDiffFile(String),
|
||||||
|
|
||||||
|
#[error("A file {0} does not seem to be a patch diff file because it has no {AGG_TILES_HASH_BEFORE_APPLY} and {AGG_TILES_HASH_AFTER_APPLY} metadata entries. These entries are automatically created when using `mbtiles diff` and `mbitiles copy --diff-with-file`. Use `--force` to ignore this warning.")]
|
||||||
|
PatchFileHasNoHashes(String),
|
||||||
|
|
||||||
|
#[error("A file {0} does not have {AGG_TILES_HASH_BEFORE_APPLY} metadata, probably because it was created by an older version of the `mbtiles` tool. Use `--force` to ignore this warning, but ensure you are applying the patch to the right file.")]
|
||||||
|
PatchFileHasNoBeforeHash(String),
|
||||||
|
|
||||||
|
#[error("The {AGG_TILES_HASH_BEFORE_APPLY}='{1}' in patch file {0} does not match {AGG_TILES_HASH}='{3}' in the file {2}")]
|
||||||
|
AggHashMismatchWithDiff(String, String, String, String),
|
||||||
|
|
||||||
|
#[error("The {AGG_TILES_HASH_AFTER_APPLY}='{1}' in patch file {0} does not match {AGG_TILES_HASH}='{3}' in the file {2} after the patch was applied")]
|
||||||
|
AggHashMismatchAfterApply(String, String, String, String),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type MbtResult<T> = Result<T, MbtError>;
|
pub type MbtResult<T> = Result<T, MbtError>;
|
||||||
|
@ -32,7 +32,7 @@ pub use update::UpdateZoomType;
|
|||||||
mod validation;
|
mod validation;
|
||||||
pub use validation::{
|
pub use validation::{
|
||||||
calc_agg_tiles_hash, AggHashType, IntegrityCheckType, MbtType, AGG_TILES_HASH,
|
calc_agg_tiles_hash, AggHashType, IntegrityCheckType, MbtType, AGG_TILES_HASH,
|
||||||
AGG_TILES_HASH_AFTER_APPLY,
|
AGG_TILES_HASH_AFTER_APPLY, AGG_TILES_HASH_BEFORE_APPLY,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// `MBTiles` uses a TMS (Tile Map Service) scheme for its tile coordinates (inverted along the Y axis).
|
/// `MBTiles` uses a TMS (Tile Map Service) scheme for its tile coordinates (inverted along the Y axis).
|
||||||
|
@ -42,6 +42,13 @@ impl CopyType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct PatchFileInfo {
|
||||||
|
pub mbt_type: MbtType,
|
||||||
|
pub agg_tiles_hash: Option<String>,
|
||||||
|
pub agg_tiles_hash_before_apply: Option<String>,
|
||||||
|
pub agg_tiles_hash_after_apply: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Mbtiles {
|
pub struct Mbtiles {
|
||||||
filepath: String,
|
filepath: String,
|
||||||
@ -212,11 +219,6 @@ impl Mbtiles {
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn open_and_detect_type(&self) -> MbtResult<MbtType> {
|
|
||||||
let mut conn = self.open_readonly().await?;
|
|
||||||
self.detect_type(&mut conn).await
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn attach_hash_fn(conn: &mut SqliteConnection) -> MbtResult<()> {
|
pub async fn attach_hash_fn(conn: &mut SqliteConnection) -> MbtResult<()> {
|
||||||
|
@ -1,25 +1,53 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use log::{debug, info};
|
use log::{debug, info, warn};
|
||||||
use sqlx::query;
|
use sqlx::{query, Connection as _};
|
||||||
|
|
||||||
use crate::queries::detach_db;
|
use crate::queries::detach_db;
|
||||||
use crate::MbtType::{Flat, FlatWithHash, Normalized};
|
use crate::MbtType::{Flat, FlatWithHash, Normalized};
|
||||||
use crate::{MbtResult, MbtType, Mbtiles, AGG_TILES_HASH, AGG_TILES_HASH_AFTER_APPLY};
|
use crate::{
|
||||||
|
MbtError, MbtResult, MbtType, Mbtiles, AGG_TILES_HASH, AGG_TILES_HASH_AFTER_APPLY,
|
||||||
|
AGG_TILES_HASH_BEFORE_APPLY,
|
||||||
|
};
|
||||||
|
|
||||||
pub async fn apply_patch(base_file: PathBuf, patch_file: PathBuf) -> MbtResult<()> {
|
pub async fn apply_patch(base_file: PathBuf, patch_file: PathBuf, force: bool) -> MbtResult<()> {
|
||||||
let base_mbt = Mbtiles::new(base_file)?;
|
let base_mbt = Mbtiles::new(base_file)?;
|
||||||
let patch_mbt = Mbtiles::new(patch_file)?;
|
let patch_mbt = Mbtiles::new(patch_file)?;
|
||||||
let patch_type = patch_mbt.open_and_detect_type().await?;
|
|
||||||
|
let mut conn = patch_mbt.open_readonly().await?;
|
||||||
|
let patch_info = patch_mbt.examine_diff(&mut conn).await?;
|
||||||
|
patch_mbt.validate_diff_info(&patch_info, force)?;
|
||||||
|
let patch_type = patch_info.mbt_type;
|
||||||
|
conn.close().await?;
|
||||||
|
|
||||||
let mut conn = base_mbt.open().await?;
|
let mut conn = base_mbt.open().await?;
|
||||||
let base_type = base_mbt.detect_type(&mut conn).await?;
|
let base_info = base_mbt.examine_diff(&mut conn).await?;
|
||||||
|
let base_hash = base_mbt.get_agg_tiles_hash(&mut conn).await?;
|
||||||
|
base_mbt.assert_hashes(&base_info, force)?;
|
||||||
|
|
||||||
info!("Applying patch file {patch_mbt} ({patch_type}) to {base_mbt} ({base_type})");
|
match (force, base_hash, patch_info.agg_tiles_hash_before_apply) {
|
||||||
|
(false, Some(base_hash), Some(expected_hash)) if base_hash != expected_hash => {
|
||||||
|
return Err(MbtError::AggHashMismatchWithDiff(
|
||||||
|
patch_mbt.filepath().to_string(),
|
||||||
|
expected_hash,
|
||||||
|
base_mbt.filepath().to_string(),
|
||||||
|
base_hash,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
(true, Some(base_hash), Some(expected_hash)) if base_hash != expected_hash => {
|
||||||
|
warn!("Aggregate tiles hash mismatch: Patch file expected {expected_hash} but found {base_hash} in {base_mbt} (force mode)");
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"Applying patch file {patch_mbt} ({patch_type}) to {base_mbt} ({base_type})",
|
||||||
|
base_type = base_info.mbt_type
|
||||||
|
);
|
||||||
|
|
||||||
patch_mbt.attach_to(&mut conn, "patchDb").await?;
|
patch_mbt.attach_to(&mut conn, "patchDb").await?;
|
||||||
let select_from = get_select_from(base_type, patch_type);
|
let select_from = get_select_from(base_info.mbt_type, patch_type);
|
||||||
let (main_table, insert1, insert2) = get_insert_sql(base_type, select_from);
|
let (main_table, insert1, insert2) = get_insert_sql(base_info.mbt_type, select_from);
|
||||||
|
|
||||||
let sql = format!("{insert1} WHERE tile_data NOTNULL");
|
let sql = format!("{insert1} WHERE tile_data NOTNULL");
|
||||||
query(&sql).execute(&mut conn).await?;
|
query(&sql).execute(&mut conn).await?;
|
||||||
@ -38,7 +66,7 @@ pub async fn apply_patch(base_file: PathBuf, patch_file: PathBuf) -> MbtResult<(
|
|||||||
);
|
);
|
||||||
query(&sql).execute(&mut conn).await?;
|
query(&sql).execute(&mut conn).await?;
|
||||||
|
|
||||||
if base_type.is_normalized() {
|
if base_info.mbt_type.is_normalized() {
|
||||||
debug!("Removing unused tiles from the images table (normalized schema)");
|
debug!("Removing unused tiles from the images table (normalized schema)");
|
||||||
let sql = "DELETE FROM images WHERE tile_id NOT IN (SELECT tile_id FROM map)";
|
let sql = "DELETE FROM images WHERE tile_id NOT IN (SELECT tile_id FROM map)";
|
||||||
query(sql).execute(&mut conn).await?;
|
query(sql).execute(&mut conn).await?;
|
||||||
@ -53,7 +81,7 @@ pub async fn apply_patch(base_file: PathBuf, patch_file: PathBuf) -> MbtResult<(
|
|||||||
SELECT IIF(name = '{AGG_TILES_HASH_AFTER_APPLY}', '{AGG_TILES_HASH}', name) as name,
|
SELECT IIF(name = '{AGG_TILES_HASH_AFTER_APPLY}', '{AGG_TILES_HASH}', name) as name,
|
||||||
value
|
value
|
||||||
FROM patchDb.metadata
|
FROM patchDb.metadata
|
||||||
WHERE name NOTNULL AND name != '{AGG_TILES_HASH}';"
|
WHERE name NOTNULL AND name NOT IN ('{AGG_TILES_HASH}', '{AGG_TILES_HASH_BEFORE_APPLY}');"
|
||||||
);
|
);
|
||||||
query(&sql).execute(&mut conn).await?;
|
query(&sql).execute(&mut conn).await?;
|
||||||
|
|
||||||
@ -151,7 +179,7 @@ mod tests {
|
|||||||
|
|
||||||
// Apply patch to the src data in in-memory DB
|
// Apply patch to the src data in in-memory DB
|
||||||
let patch_file = PathBuf::from("../tests/fixtures/mbtiles/world_cities_diff.mbtiles");
|
let patch_file = PathBuf::from("../tests/fixtures/mbtiles/world_cities_diff.mbtiles");
|
||||||
apply_patch(src, patch_file).await?;
|
apply_patch(src, patch_file, true).await?;
|
||||||
|
|
||||||
// Verify the data is the same as the file the patch was generated from
|
// Verify the data is the same as the file the patch was generated from
|
||||||
Mbtiles::new("../tests/fixtures/mbtiles/world_cities_modified.mbtiles")?
|
Mbtiles::new("../tests/fixtures/mbtiles/world_cities_modified.mbtiles")?
|
||||||
@ -183,7 +211,7 @@ mod tests {
|
|||||||
// Apply patch to the src data in in-memory DB
|
// Apply patch to the src data in in-memory DB
|
||||||
let patch_file =
|
let patch_file =
|
||||||
PathBuf::from("../tests/fixtures/mbtiles/geography-class-jpg-diff.mbtiles");
|
PathBuf::from("../tests/fixtures/mbtiles/geography-class-jpg-diff.mbtiles");
|
||||||
apply_patch(src, patch_file).await?;
|
apply_patch(src, patch_file, true).await?;
|
||||||
|
|
||||||
// Verify the data is the same as the file the patch was generated from
|
// Verify the data is the same as the file the patch was generated from
|
||||||
Mbtiles::new("../tests/fixtures/mbtiles/geography-class-jpg-modified.mbtiles")?
|
Mbtiles::new("../tests/fixtures/mbtiles/geography-class-jpg-modified.mbtiles")?
|
||||||
|
@ -7,10 +7,11 @@ use martin_tile_utils::{Format, TileInfo, MAX_ZOOM};
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use sqlx::sqlite::SqliteRow;
|
use sqlx::sqlite::SqliteRow;
|
||||||
use sqlx::{query, Row, SqliteExecutor};
|
use sqlx::{query, Row, SqliteConnection, SqliteExecutor};
|
||||||
use tilejson::TileJSON;
|
use tilejson::TileJSON;
|
||||||
|
|
||||||
use crate::errors::{MbtError, MbtResult};
|
use crate::errors::{MbtError, MbtResult};
|
||||||
|
use crate::mbtiles::PatchFileInfo;
|
||||||
use crate::queries::{
|
use crate::queries::{
|
||||||
has_tiles_with_hash, is_flat_tables_type, is_flat_with_hash_tables_type,
|
has_tiles_with_hash, is_flat_tables_type, is_flat_with_hash_tables_type,
|
||||||
is_normalized_tables_type,
|
is_normalized_tables_type,
|
||||||
@ -27,6 +28,9 @@ pub const AGG_TILES_HASH: &str = "agg_tiles_hash";
|
|||||||
/// Metadata key for a diff file, describing the eventual [`AGG_TILES_HASH`] value of the resulting tileset once the diff is applied
|
/// Metadata key for a diff file, describing the eventual [`AGG_TILES_HASH`] value of the resulting tileset once the diff is applied
|
||||||
pub const AGG_TILES_HASH_AFTER_APPLY: &str = "agg_tiles_hash_after_apply";
|
pub const AGG_TILES_HASH_AFTER_APPLY: &str = "agg_tiles_hash_after_apply";
|
||||||
|
|
||||||
|
/// Metadata key for a diff file, describing the expected [`AGG_TILES_HASH`] value of the tileset to which the diff will be applied.
|
||||||
|
pub const AGG_TILES_HASH_BEFORE_APPLY: &str = "agg_tiles_hash_before_apply";
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, EnumDisplay, Serialize)]
|
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, EnumDisplay, Serialize)]
|
||||||
#[enum_display(case = "Kebab")]
|
#[enum_display(case = "Kebab")]
|
||||||
pub enum MbtType {
|
pub enum MbtType {
|
||||||
@ -71,7 +75,7 @@ pub enum AggHashType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Mbtiles {
|
impl Mbtiles {
|
||||||
pub async fn validate(
|
pub async fn open_and_validate(
|
||||||
&self,
|
&self,
|
||||||
check_type: IntegrityCheckType,
|
check_type: IntegrityCheckType,
|
||||||
agg_hash: AggHashType,
|
agg_hash: AggHashType,
|
||||||
@ -81,12 +85,24 @@ impl Mbtiles {
|
|||||||
} else {
|
} else {
|
||||||
self.open_readonly().await?
|
self.open_readonly().await?
|
||||||
};
|
};
|
||||||
self.check_integrity(&mut conn, check_type).await?;
|
self.validate(&mut conn, check_type, agg_hash).await
|
||||||
self.check_tiles_type_validity(&mut conn).await?;
|
}
|
||||||
self.check_each_tile_hash(&mut conn).await?;
|
|
||||||
|
pub async fn validate<T>(
|
||||||
|
&self,
|
||||||
|
conn: &mut T,
|
||||||
|
check_type: IntegrityCheckType,
|
||||||
|
agg_hash: AggHashType,
|
||||||
|
) -> MbtResult<String>
|
||||||
|
where
|
||||||
|
for<'e> &'e mut T: SqliteExecutor<'e>,
|
||||||
|
{
|
||||||
|
self.check_integrity(&mut *conn, check_type).await?;
|
||||||
|
self.check_tiles_type_validity(&mut *conn).await?;
|
||||||
|
self.check_each_tile_hash(&mut *conn).await?;
|
||||||
match agg_hash {
|
match agg_hash {
|
||||||
AggHashType::Verify => self.check_agg_tiles_hashes(&mut conn).await,
|
AggHashType::Verify => self.check_agg_tiles_hashes(conn).await,
|
||||||
AggHashType::Update => self.update_agg_tiles_hash(&mut conn).await,
|
AggHashType::Update => self.update_agg_tiles_hash(conn).await,
|
||||||
AggHashType::Off => Ok(String::new()),
|
AggHashType::Off => Ok(String::new()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -453,6 +469,69 @@ LIMIT 1;"
|
|||||||
info!("All tile hashes are valid for {self}");
|
info!("All tile hashes are valid for {self}");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn examine_diff(&self, conn: &mut SqliteConnection) -> MbtResult<PatchFileInfo> {
|
||||||
|
let info = PatchFileInfo {
|
||||||
|
mbt_type: self.detect_type(&mut *conn).await?,
|
||||||
|
agg_tiles_hash: self.get_agg_tiles_hash(&mut *conn).await?,
|
||||||
|
agg_tiles_hash_before_apply: self
|
||||||
|
.get_metadata_value(&mut *conn, AGG_TILES_HASH_BEFORE_APPLY)
|
||||||
|
.await?,
|
||||||
|
agg_tiles_hash_after_apply: self
|
||||||
|
.get_metadata_value(&mut *conn, AGG_TILES_HASH_AFTER_APPLY)
|
||||||
|
.await?,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(info)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn assert_hashes(&self, info: &PatchFileInfo, force: bool) -> MbtResult<()> {
|
||||||
|
if info.agg_tiles_hash.is_none() {
|
||||||
|
if !force {
|
||||||
|
return Err(MbtError::CannotDiffFileWithoutHash(
|
||||||
|
self.filepath().to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
warn!("File {self} has no {AGG_TILES_HASH} metadata field, probably because it was created by an older version of the `mbtiles` tool. Use this command to update the value:\nmbtiles validate --agg-hash update {self}");
|
||||||
|
} else if info.agg_tiles_hash_before_apply.is_some()
|
||||||
|
|| info.agg_tiles_hash_after_apply.is_some()
|
||||||
|
{
|
||||||
|
if !force {
|
||||||
|
return Err(MbtError::DiffingDiffFile(self.filepath().to_string()));
|
||||||
|
}
|
||||||
|
warn!("File {self} has {AGG_TILES_HASH_BEFORE_APPLY} or {AGG_TILES_HASH_AFTER_APPLY} metadata field, indicating it is a patch file which should not be diffed with another file.");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_diff_info(&self, info: &PatchFileInfo, force: bool) -> MbtResult<()> {
|
||||||
|
match (
|
||||||
|
&info.agg_tiles_hash_before_apply,
|
||||||
|
&info.agg_tiles_hash_after_apply,
|
||||||
|
) {
|
||||||
|
(Some(before), Some(after)) => {
|
||||||
|
info!(
|
||||||
|
"The patch file {self} expects to be applied to a tileset with {AGG_TILES_HASH}={before}, and should result in hash {after} after applying",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
(None, Some(_)) => {
|
||||||
|
if !force {
|
||||||
|
return Err(MbtError::PatchFileHasNoBeforeHash(
|
||||||
|
self.filepath().to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
warn!(
|
||||||
|
"The patch file {self} has no {AGG_TILES_HASH_BEFORE_APPLY} metadata field, probably because it was created by an older version of the `mbtiles` tool.");
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
if !force {
|
||||||
|
return Err(MbtError::PatchFileHasNoHashes(self.filepath().to_string()));
|
||||||
|
}
|
||||||
|
warn!("The patch file {self} has no {AGG_TILES_HASH_AFTER_APPLY} metadata field, probably because it was not properly created by the `mbtiles` tool.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute the hash of the combined tiles in the mbtiles file tiles table/view.
|
/// Compute the hash of the combined tiles in the mbtiles file tiles table/view.
|
||||||
|
@ -242,7 +242,7 @@ fn databases() -> Databases {
|
|||||||
copy!(result.path("empty_no_hash", mbt_typ), path(&empty_mbt));
|
copy!(result.path("empty_no_hash", mbt_typ), path(&empty_mbt));
|
||||||
let dmp = dump(&mut empty_cn).await.unwrap();
|
let dmp = dump(&mut empty_cn).await.unwrap();
|
||||||
assert_dump!(&dmp, "{typ}__empty");
|
assert_dump!(&dmp, "{typ}__empty");
|
||||||
let hash = empty_mbt.validate(Off, Verify).await.unwrap();
|
let hash = empty_mbt.open_and_validate(Off, Verify).await.unwrap();
|
||||||
allow_duplicates! {
|
allow_duplicates! {
|
||||||
assert_snapshot!(hash, @"D41D8CD98F00B204E9800998ECF8427E");
|
assert_snapshot!(hash, @"D41D8CD98F00B204E9800998ECF8427E");
|
||||||
}
|
}
|
||||||
@ -265,7 +265,7 @@ fn databases() -> Databases {
|
|||||||
copy!(result.path("v1_no_hash", mbt_typ), path(&v1_mbt));
|
copy!(result.path("v1_no_hash", mbt_typ), path(&v1_mbt));
|
||||||
let dmp = dump(&mut v1_cn).await.unwrap();
|
let dmp = dump(&mut v1_cn).await.unwrap();
|
||||||
assert_dump!(&dmp, "{typ}__v1");
|
assert_dump!(&dmp, "{typ}__v1");
|
||||||
let hash = v1_mbt.validate(Off, Verify).await.unwrap();
|
let hash = v1_mbt.open_and_validate(Off, Verify).await.unwrap();
|
||||||
allow_duplicates! {
|
allow_duplicates! {
|
||||||
assert_snapshot!(hash, @"9ED9178D7025276336C783C2B54D6258");
|
assert_snapshot!(hash, @"9ED9178D7025276336C783C2B54D6258");
|
||||||
}
|
}
|
||||||
@ -276,7 +276,7 @@ fn databases() -> Databases {
|
|||||||
new_file!(databases, mbt_typ, METADATA_V2, TILES_V2, "{typ}__v2");
|
new_file!(databases, mbt_typ, METADATA_V2, TILES_V2, "{typ}__v2");
|
||||||
let dmp = dump(&mut v2_cn).await.unwrap();
|
let dmp = dump(&mut v2_cn).await.unwrap();
|
||||||
assert_dump!(&dmp, "{typ}__v2");
|
assert_dump!(&dmp, "{typ}__v2");
|
||||||
let hash = v2_mbt.validate(Off, Verify).await.unwrap();
|
let hash = v2_mbt.open_and_validate(Off, Verify).await.unwrap();
|
||||||
allow_duplicates! {
|
allow_duplicates! {
|
||||||
assert_snapshot!(hash, @"3BCDEE3F52407FF1315629298CB99133");
|
assert_snapshot!(hash, @"3BCDEE3F52407FF1315629298CB99133");
|
||||||
}
|
}
|
||||||
@ -291,7 +291,7 @@ fn databases() -> Databases {
|
|||||||
};
|
};
|
||||||
let dmp = dump(&mut dif_cn).await.unwrap();
|
let dmp = dump(&mut dif_cn).await.unwrap();
|
||||||
assert_dump!(&dmp, "{typ}__dif");
|
assert_dump!(&dmp, "{typ}__dif");
|
||||||
let hash = dif_mbt.validate(Off, Verify).await.unwrap();
|
let hash = dif_mbt.open_and_validate(Off, Verify).await.unwrap();
|
||||||
allow_duplicates! {
|
allow_duplicates! {
|
||||||
assert_snapshot!(hash, @"B86122579EDCDD4C51F3910894FCC1A1");
|
assert_snapshot!(hash, @"B86122579EDCDD4C51F3910894FCC1A1");
|
||||||
}
|
}
|
||||||
@ -300,7 +300,7 @@ fn databases() -> Databases {
|
|||||||
// ----------------- v1_clone -----------------
|
// ----------------- v1_clone -----------------
|
||||||
let (v1_clone_mbt, v1_clone_cn) = open!(databases, "{typ}__v1-clone");
|
let (v1_clone_mbt, v1_clone_cn) = open!(databases, "{typ}__v1-clone");
|
||||||
let dmp = copy_dump!(result.path("v1", mbt_typ), path(&v1_clone_mbt));
|
let dmp = copy_dump!(result.path("v1", mbt_typ), path(&v1_clone_mbt));
|
||||||
let hash = v1_clone_mbt.validate(Off, Verify).await.unwrap();
|
let hash = v1_clone_mbt.open_and_validate(Off, Verify).await.unwrap();
|
||||||
allow_duplicates! {
|
allow_duplicates! {
|
||||||
assert_snapshot!(hash, @"9ED9178D7025276336C783C2B54D6258");
|
assert_snapshot!(hash, @"9ED9178D7025276336C783C2B54D6258");
|
||||||
}
|
}
|
||||||
@ -322,7 +322,7 @@ fn databases() -> Databases {
|
|||||||
};
|
};
|
||||||
let dmp = dump(&mut dif_empty_cn).await.unwrap();
|
let dmp = dump(&mut dif_empty_cn).await.unwrap();
|
||||||
assert_dump!(&dmp, "{typ}__dif_empty");
|
assert_dump!(&dmp, "{typ}__dif_empty");
|
||||||
let hash = dif_empty_mbt.validate(Off, Verify).await.unwrap();
|
let hash = dif_empty_mbt.open_and_validate(Off, Verify).await.unwrap();
|
||||||
allow_duplicates! {
|
allow_duplicates! {
|
||||||
assert_snapshot!(hash, @"D41D8CD98F00B204E9800998ECF8427E");
|
assert_snapshot!(hash, @"D41D8CD98F00B204E9800998ECF8427E");
|
||||||
}
|
}
|
||||||
@ -483,8 +483,8 @@ async fn diff_and_patch(
|
|||||||
eprintln!("TEST: Applying the difference ({b_db} - {a_db} = {dif_db}) to {a_db}, should get {b_db}");
|
eprintln!("TEST: Applying the difference ({b_db} - {a_db} = {dif_db}) to {a_db}, should get {b_db}");
|
||||||
let (clone_mbt, mut clone_cn) = open!(diff_and_patch, "{prefix}__1");
|
let (clone_mbt, mut clone_cn) = open!(diff_and_patch, "{prefix}__1");
|
||||||
copy!(databases.path(a_db, *dst_type), path(&clone_mbt));
|
copy!(databases.path(a_db, *dst_type), path(&clone_mbt));
|
||||||
apply_patch(path(&clone_mbt), path(&dif_mbt)).await?;
|
apply_patch(path(&clone_mbt), path(&dif_mbt), false).await?;
|
||||||
let hash = clone_mbt.validate(Off, Verify).await?;
|
let hash = clone_mbt.open_and_validate(Off, Verify).await?;
|
||||||
assert_eq!(hash, databases.hash(b_db, *dst_type));
|
assert_eq!(hash, databases.hash(b_db, *dst_type));
|
||||||
let dmp = dump(&mut clone_cn).await?;
|
let dmp = dump(&mut clone_cn).await?;
|
||||||
pretty_assert_eq!(&dmp, expected_b);
|
pretty_assert_eq!(&dmp, expected_b);
|
||||||
@ -492,8 +492,8 @@ async fn diff_and_patch(
|
|||||||
eprintln!("TEST: Applying the difference ({b_db} - {a_db} = {dif_db}) to {b_db}, should not modify it");
|
eprintln!("TEST: Applying the difference ({b_db} - {a_db} = {dif_db}) to {b_db}, should not modify it");
|
||||||
let (clone_mbt, mut clone_cn) = open!(diff_and_patch, "{prefix}__2");
|
let (clone_mbt, mut clone_cn) = open!(diff_and_patch, "{prefix}__2");
|
||||||
copy!(databases.path(b_db, *dst_type), path(&clone_mbt));
|
copy!(databases.path(b_db, *dst_type), path(&clone_mbt));
|
||||||
apply_patch(path(&clone_mbt), path(&dif_mbt)).await?;
|
apply_patch(path(&clone_mbt), path(&dif_mbt), true).await?;
|
||||||
let hash = clone_mbt.validate(Off, Verify).await?;
|
let hash = clone_mbt.open_and_validate(Off, Verify).await?;
|
||||||
assert_eq!(hash, databases.hash(b_db, *dst_type));
|
assert_eq!(hash, databases.hash(b_db, *dst_type));
|
||||||
let dmp = dump(&mut clone_cn).await?;
|
let dmp = dump(&mut clone_cn).await?;
|
||||||
pretty_assert_eq!(&dmp, expected_b);
|
pretty_assert_eq!(&dmp, expected_b);
|
||||||
@ -522,10 +522,9 @@ async fn patch_on_copy(
|
|||||||
apply_patch => Some(databases.path("dif", dif_type)),
|
apply_patch => Some(databases.path("dif", dif_type)),
|
||||||
dst_type_cli => v2_type,
|
dst_type_cli => v2_type,
|
||||||
};
|
};
|
||||||
pretty_assert_eq!(
|
let actual = dump(&mut v2_cn).await?;
|
||||||
&dump(&mut v2_cn).await?,
|
let expected = databases.dump("v2", v2_type.unwrap_or(v1_type));
|
||||||
databases.dump("v2", v2_type.unwrap_or(v1_type))
|
pretty_assert_eq!(&actual, expected);
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ CREATE TABLE metadata (
|
|||||||
values = [
|
values = [
|
||||||
'( "agg_tiles_hash", "B86122579EDCDD4C51F3910894FCC1A1" )',
|
'( "agg_tiles_hash", "B86122579EDCDD4C51F3910894FCC1A1" )',
|
||||||
'( "agg_tiles_hash_after_apply", "3BCDEE3F52407FF1315629298CB99133" )',
|
'( "agg_tiles_hash_after_apply", "3BCDEE3F52407FF1315629298CB99133" )',
|
||||||
|
'( "agg_tiles_hash_before_apply", "9ED9178D7025276336C783C2B54D6258" )',
|
||||||
'( "md-edit", "value - v2" )',
|
'( "md-edit", "value - v2" )',
|
||||||
'( "md-new", "value - new" )',
|
'( "md-new", "value - new" )',
|
||||||
'( "md-remove", NULL )',
|
'( "md-remove", NULL )',
|
||||||
|
@ -12,6 +12,7 @@ CREATE TABLE metadata (
|
|||||||
values = [
|
values = [
|
||||||
'( "agg_tiles_hash", "D41D8CD98F00B204E9800998ECF8427E" )',
|
'( "agg_tiles_hash", "D41D8CD98F00B204E9800998ECF8427E" )',
|
||||||
'( "agg_tiles_hash_after_apply", "9ED9178D7025276336C783C2B54D6258" )',
|
'( "agg_tiles_hash_after_apply", "9ED9178D7025276336C783C2B54D6258" )',
|
||||||
|
'( "agg_tiles_hash_before_apply", "9ED9178D7025276336C783C2B54D6258" )',
|
||||||
]
|
]
|
||||||
|
|
||||||
[[]]
|
[[]]
|
||||||
|
@ -12,6 +12,7 @@ CREATE TABLE metadata (
|
|||||||
values = [
|
values = [
|
||||||
'( "agg_tiles_hash", "B86122579EDCDD4C51F3910894FCC1A1" )',
|
'( "agg_tiles_hash", "B86122579EDCDD4C51F3910894FCC1A1" )',
|
||||||
'( "agg_tiles_hash_after_apply", "3BCDEE3F52407FF1315629298CB99133" )',
|
'( "agg_tiles_hash_after_apply", "3BCDEE3F52407FF1315629298CB99133" )',
|
||||||
|
'( "agg_tiles_hash_before_apply", "9ED9178D7025276336C783C2B54D6258" )',
|
||||||
'( "md-edit", "value - v2" )',
|
'( "md-edit", "value - v2" )',
|
||||||
'( "md-new", "value - new" )',
|
'( "md-new", "value - new" )',
|
||||||
'( "md-remove", NULL )',
|
'( "md-remove", NULL )',
|
||||||
|
@ -12,6 +12,7 @@ CREATE TABLE metadata (
|
|||||||
values = [
|
values = [
|
||||||
'( "agg_tiles_hash", "D41D8CD98F00B204E9800998ECF8427E" )',
|
'( "agg_tiles_hash", "D41D8CD98F00B204E9800998ECF8427E" )',
|
||||||
'( "agg_tiles_hash_after_apply", "9ED9178D7025276336C783C2B54D6258" )',
|
'( "agg_tiles_hash_after_apply", "9ED9178D7025276336C783C2B54D6258" )',
|
||||||
|
'( "agg_tiles_hash_before_apply", "9ED9178D7025276336C783C2B54D6258" )',
|
||||||
]
|
]
|
||||||
|
|
||||||
[[]]
|
[[]]
|
||||||
|
@ -50,6 +50,7 @@ CREATE TABLE metadata (
|
|||||||
values = [
|
values = [
|
||||||
'( "agg_tiles_hash", "B86122579EDCDD4C51F3910894FCC1A1" )',
|
'( "agg_tiles_hash", "B86122579EDCDD4C51F3910894FCC1A1" )',
|
||||||
'( "agg_tiles_hash_after_apply", "3BCDEE3F52407FF1315629298CB99133" )',
|
'( "agg_tiles_hash_after_apply", "3BCDEE3F52407FF1315629298CB99133" )',
|
||||||
|
'( "agg_tiles_hash_before_apply", "9ED9178D7025276336C783C2B54D6258" )',
|
||||||
'( "md-edit", "value - v2" )',
|
'( "md-edit", "value - v2" )',
|
||||||
'( "md-new", "value - new" )',
|
'( "md-new", "value - new" )',
|
||||||
'( "md-remove", NULL )',
|
'( "md-remove", NULL )',
|
||||||
|
@ -33,6 +33,7 @@ CREATE TABLE metadata (
|
|||||||
values = [
|
values = [
|
||||||
'( "agg_tiles_hash", "D41D8CD98F00B204E9800998ECF8427E" )',
|
'( "agg_tiles_hash", "D41D8CD98F00B204E9800998ECF8427E" )',
|
||||||
'( "agg_tiles_hash_after_apply", "9ED9178D7025276336C783C2B54D6258" )',
|
'( "agg_tiles_hash_after_apply", "9ED9178D7025276336C783C2B54D6258" )',
|
||||||
|
'( "agg_tiles_hash_before_apply", "9ED9178D7025276336C783C2B54D6258" )',
|
||||||
]
|
]
|
||||||
|
|
||||||
[[]]
|
[[]]
|
||||||
|
@ -109,4 +109,5 @@ json:
|
|||||||
count: 68
|
count: 68
|
||||||
geometry: Point
|
geometry: Point
|
||||||
layer: cities
|
layer: cities
|
||||||
|
agg_tiles_hash: 84792BF4EE9AEDDC5B1A60E707011FEE
|
||||||
|
|
||||||
|
BIN
tests/fixtures/mbtiles/world_cities.mbtiles
vendored
BIN
tests/fixtures/mbtiles/world_cities.mbtiles
vendored
Binary file not shown.
BIN
tests/fixtures/mbtiles/world_cities_modified.mbtiles
vendored
BIN
tests/fixtures/mbtiles/world_cities_modified.mbtiles
vendored
Binary file not shown.
Loading…
Reference in New Issue
Block a user