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 use crate::metadata::temp_named_mbtiles;
171
172 #[actix_rt::test]
173 async fn apply_flat_patch_file() {
174 let script = include_str!("../../tests/fixtures/mbtiles/world_cities.sql");
176 let (_mbt, _conn, src_file) = temp_named_mbtiles("flat_src_file_mem", script).await;
177
178 let dst_file = PathBuf::from("file:apply_flat_patch_file?mode=memory&cache=shared");
179
180 let mut src_conn = MbtilesCopier {
181 src_file: src_file.clone(),
182 dst_file: dst_file.clone(),
183 ..Default::default()
184 }
185 .run()
186 .await
187 .unwrap();
188
189 let script = include_str!("../../tests/fixtures/mbtiles/world_cities_diff.sql");
191 let (_mbt, _conn, patch_file) = temp_named_mbtiles("flat_patch_file_mem", script).await;
192 apply_patch(dst_file, patch_file, true).await.unwrap();
193
194 let script = include_str!("../../tests/fixtures/mbtiles/world_cities_modified.sql");
196 let (mbt, _conn, _) = temp_named_mbtiles("flat_attached_mem_db", script).await;
197 mbt.attach_to(&mut src_conn, "testOtherDb").await.unwrap();
198
199 assert!(
200 src_conn
201 .fetch_optional("SELECT * FROM tiles EXCEPT SELECT * FROM testOtherDb.tiles;")
202 .await
203 .unwrap()
204 .is_none()
205 );
206 }
207
208 #[actix_rt::test]
209 async fn apply_normalized_patch_file() {
210 let script = include_str!("../../tests/fixtures/mbtiles/geography-class-jpg.sql");
212 let (_mbt, _conn, src_file) = temp_named_mbtiles("normalized_src_file_mem", script).await;
213
214 let dst_file =
215 PathBuf::from("file:apply_normalized_diff_file_mem_db?mode=memory&cache=shared");
216
217 let mut src_conn = MbtilesCopier {
218 src_file: src_file.clone(),
219 dst_file: dst_file.clone(),
220 ..Default::default()
221 }
222 .run()
223 .await
224 .unwrap();
225
226 let script = include_str!("../../tests/fixtures/mbtiles/geography-class-jpg-diff.sql");
228 let (_mbt, _conn, patch_file) =
229 temp_named_mbtiles("normalized_patch_file_mem", script).await;
230 apply_patch(dst_file, patch_file, true).await.unwrap();
231
232 let script = include_str!("../../tests/fixtures/mbtiles/geography-class-jpg-modified.sql");
234 let (mbt, _conn, _) = temp_named_mbtiles("normalized_attached_mem_db", script).await;
235 mbt.attach_to(&mut src_conn, "testOtherDb").await.unwrap();
236
237 assert!(
238 src_conn
239 .fetch_optional("SELECT * FROM tiles EXCEPT SELECT * FROM testOtherDb.tiles;")
240 .await
241 .unwrap()
242 .is_none()
243 );
244 }
245}