1use std::path::PathBuf;
2
3use log::{debug, info, warn};
4use sqlx::{Connection as _, query};
5
6use crate::MbtType::{Flat, FlatWithHash, Normalized};
7use crate::queries::detach_db;
8use crate::{
9 AGG_TILES_HASH, AGG_TILES_HASH_AFTER_APPLY, AGG_TILES_HASH_BEFORE_APPLY, MbtError, MbtResult,
10 MbtType, Mbtiles,
11};
12
13pub async fn apply_patch(base_file: PathBuf, patch_file: PathBuf, force: bool) -> MbtResult<()> {
14 let base_mbt = Mbtiles::new(base_file)?;
15 let patch_mbt = Mbtiles::new(patch_file)?;
16
17 let mut conn = patch_mbt.open_readonly().await?;
18 let patch_info = patch_mbt.examine_diff(&mut conn).await?;
19 if patch_info.patch_type.is_some() {
20 return Err(MbtError::UnsupportedPatchType);
21 }
22 patch_mbt.validate_diff_info(&patch_info, force)?;
23 let patch_type = patch_info.mbt_type;
24 conn.close().await?;
25
26 let mut conn = base_mbt.open().await?;
27 let base_info = base_mbt.examine_diff(&mut conn).await?;
28 let base_hash = base_mbt.get_agg_tiles_hash(&mut conn).await?;
29 base_mbt.assert_hashes(&base_info, force)?;
30
31 match (force, base_hash, patch_info.agg_tiles_hash_before_apply) {
32 (false, Some(base_hash), Some(expected_hash)) if base_hash != expected_hash => {
33 return Err(MbtError::AggHashMismatchWithDiff(
34 patch_mbt.filepath().to_string(),
35 expected_hash,
36 base_mbt.filepath().to_string(),
37 base_hash,
38 ));
39 }
40 (true, Some(base_hash), Some(expected_hash)) if base_hash != expected_hash => {
41 warn!(
42 "Aggregate tiles hash mismatch: Patch file expected {expected_hash} but found {base_hash} in {base_mbt} (force mode)"
43 );
44 }
45 _ => {}
46 }
47
48 info!(
49 "Applying patch file {patch_mbt} ({patch_type}) to {base_mbt} ({base_type})",
50 base_type = base_info.mbt_type
51 );
52
53 patch_mbt.attach_to(&mut conn, "patchDb").await?;
54 let select_from = get_select_from(base_info.mbt_type, patch_type);
55 let (main_table, insert1, insert2) = get_insert_sql(base_info.mbt_type, select_from);
56
57 let sql = format!("{insert1} WHERE tile_data NOTNULL");
58 query(&sql).execute(&mut conn).await?;
59
60 if let Some(insert2) = insert2 {
61 let sql = format!("{insert2} WHERE tile_data NOTNULL");
62 query(&sql).execute(&mut conn).await?;
63 }
64
65 let sql = format!(
66 "
67 DELETE FROM {main_table}
68 WHERE (zoom_level, tile_column, tile_row) IN (
69 SELECT zoom_level, tile_column, tile_row FROM ({select_from} WHERE tile_data ISNULL)
70 )"
71 );
72 query(&sql).execute(&mut conn).await?;
73
74 if base_info.mbt_type.is_normalized() {
75 debug!("Removing unused tiles from the images table (normalized schema)");
76 let sql = "DELETE FROM images WHERE tile_id NOT IN (SELECT tile_id FROM map)";
77 query(sql).execute(&mut conn).await?;
78 }
79
80 let sql = format!(
84 "
85 INSERT OR REPLACE INTO metadata (name, value)
86 SELECT IIF(name = '{AGG_TILES_HASH_AFTER_APPLY}', '{AGG_TILES_HASH}', name) as name,
87 value
88 FROM patchDb.metadata
89 WHERE name NOTNULL AND name NOT IN ('{AGG_TILES_HASH}', '{AGG_TILES_HASH_BEFORE_APPLY}');"
90 );
91 query(&sql).execute(&mut conn).await?;
92
93 let sql = "
94 DELETE FROM metadata
95 WHERE name IN (SELECT name FROM patchDb.metadata WHERE value ISNULL);";
96 query(sql).execute(&mut conn).await?;
97
98 detach_db(&mut conn, "patchDb").await
99}
100
101fn get_select_from(src_type: MbtType, patch_type: MbtType) -> &'static str {
102 if src_type == Flat {
103 "SELECT zoom_level, tile_column, tile_row, tile_data FROM patchDb.tiles"
104 } else {
105 match patch_type {
106 Flat => {
107 "
108 SELECT zoom_level, tile_column, tile_row, tile_data, md5_hex(tile_data) as hash
109 FROM patchDb.tiles"
110 }
111 FlatWithHash => {
112 "
113 SELECT zoom_level, tile_column, tile_row, tile_data, tile_hash AS hash
114 FROM patchDb.tiles_with_hash"
115 }
116 Normalized { .. } => {
117 "
118 SELECT zoom_level, tile_column, tile_row, tile_data, map.tile_id AS hash
119 FROM patchDb.map LEFT JOIN patchDb.images
120 ON patchDb.map.tile_id = patchDb.images.tile_id"
121 }
122 }
123 }
124}
125
126fn get_insert_sql(src_type: MbtType, select_from: &str) -> (&'static str, String, Option<String>) {
127 match src_type {
128 Flat => (
129 "tiles",
130 format!(
131 "
132 INSERT OR REPLACE INTO tiles (zoom_level, tile_column, tile_row, tile_data)
133 {select_from}"
134 ),
135 None,
136 ),
137 FlatWithHash => (
138 "tiles_with_hash",
139 format!(
140 "
141 INSERT OR REPLACE INTO tiles_with_hash (zoom_level, tile_column, tile_row, tile_data, tile_hash)
142 {select_from}"
143 ),
144 None,
145 ),
146 Normalized { .. } => (
147 "map",
148 format!(
149 "
150 INSERT OR REPLACE INTO map (zoom_level, tile_column, tile_row, tile_id)
151 SELECT zoom_level, tile_column, tile_row, hash as tile_id
152 FROM ({select_from})"
153 ),
154 Some(format!(
155 "
156 INSERT OR REPLACE INTO images (tile_id, tile_data)
157 SELECT hash as tile_id, tile_data
158 FROM ({select_from})"
159 )),
160 ),
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use sqlx::Executor as _;
167
168 use super::*;
169 use crate::MbtilesCopier;
170
171 #[actix_rt::test]
172 async fn apply_flat_patch_file() -> MbtResult<()> {
173 let src_file = PathBuf::from("../tests/fixtures/mbtiles/world_cities.mbtiles");
175 let src = PathBuf::from("file:apply_flat_diff_file_mem_db?mode=memory&cache=shared");
176
177 let mut src_conn = MbtilesCopier {
178 src_file: src_file.clone(),
179 dst_file: src.clone(),
180 ..Default::default()
181 }
182 .run()
183 .await?;
184
185 let patch_file = PathBuf::from("../tests/fixtures/mbtiles/world_cities_diff.mbtiles");
187 apply_patch(src, patch_file, true).await?;
188
189 Mbtiles::new("../tests/fixtures/mbtiles/world_cities_modified.mbtiles")?
191 .attach_to(&mut src_conn, "testOtherDb")
192 .await?;
193
194 assert!(
195 src_conn
196 .fetch_optional("SELECT * FROM tiles EXCEPT SELECT * FROM testOtherDb.tiles;")
197 .await?
198 .is_none()
199 );
200
201 Ok(())
202 }
203
204 #[actix_rt::test]
205 async fn apply_normalized_patch_file() -> MbtResult<()> {
206 let src_file = PathBuf::from("../tests/fixtures/mbtiles/geography-class-jpg.mbtiles");
208 let src = PathBuf::from("file:apply_normalized_diff_file_mem_db?mode=memory&cache=shared");
209
210 let mut src_conn = MbtilesCopier {
211 src_file: src_file.clone(),
212 dst_file: src.clone(),
213 ..Default::default()
214 }
215 .run()
216 .await?;
217
218 let patch_file =
220 PathBuf::from("../tests/fixtures/mbtiles/geography-class-jpg-diff.mbtiles");
221 apply_patch(src, patch_file, true).await?;
222
223 Mbtiles::new("../tests/fixtures/mbtiles/geography-class-jpg-modified.mbtiles")?
225 .attach_to(&mut src_conn, "testOtherDb")
226 .await?;
227
228 assert!(
229 src_conn
230 .fetch_optional("SELECT * FROM tiles EXCEPT SELECT * FROM testOtherDb.tiles;")
231 .await?
232 .is_none()
233 );
234
235 Ok(())
236 }
237}