geocoder.rs (30573B)
1 use crate::error::GeocoderError; 2 use crate::model::{ 3 GeocoderCountryListResult, GeocoderPoint, GeocoderReverseOptions, GeocoderReverseResult, 4 }; 5 use rusqlite::{Connection, named_params}; 6 use std::io::Write; 7 use std::path::Path; 8 9 pub struct Geocoder { 10 conn: Connection, 11 _temp_path: Option<tempfile::TempPath>, 12 } 13 14 impl Geocoder { 15 pub fn open_path<P: AsRef<Path>>(path: P) -> Result<Self, GeocoderError> { 16 let conn = Connection::open(path)?; 17 Ok(Self { 18 conn, 19 _temp_path: None, 20 }) 21 } 22 23 pub fn open_bytes(bytes: &[u8]) -> Result<Self, GeocoderError> { 24 let mut temp = tempfile::NamedTempFile::new()?; 25 temp.as_file_mut().write_all(bytes)?; 26 let temp_path = temp.into_temp_path(); 27 let path: &Path = temp_path.as_ref(); 28 let conn = Connection::open(path)?; 29 Ok(Self { 30 conn, 31 _temp_path: Some(temp_path), 32 }) 33 } 34 35 pub fn reverse( 36 &self, 37 point: GeocoderPoint, 38 options: Option<GeocoderReverseOptions>, 39 ) -> Result<Vec<GeocoderReverseResult>, GeocoderError> { 40 let options = options.unwrap_or_default(); 41 let lng_weight = point.lat.to_radians().cos().powi(2); 42 let mut stmt = self.conn.prepare( 43 r#" 44 SELECT 45 g.id, 46 g.name, 47 g.admin1_id, 48 g.admin1_name, 49 g.country_id, 50 g.country_name, 51 g.latitude, 52 g.longitude 53 FROM geonames AS g 54 JOIN coordinates AS c 55 ON g.id = c.feature_id 56 WHERE c.latitude BETWEEN :lat - :degree_offset AND :lat + :degree_offset 57 AND c.longitude BETWEEN :lng - :degree_offset AND :lng + :degree_offset 58 ORDER BY 59 ((:lat - c.latitude) * (:lat - c.latitude)) 60 + ((:lng - c.longitude) * (:lng - c.longitude) * :lng_weight) ASC 61 LIMIT :limit 62 "#, 63 )?; 64 let params = named_params! { 65 ":lat": point.lat, 66 ":lng": point.lng, 67 ":degree_offset": options.degree_offset, 68 ":lng_weight": lng_weight, 69 ":limit": options.limit as i64, 70 }; 71 collect_mapped_rows(&mut stmt, params, map_reverse_row) 72 } 73 74 pub fn country(&self, country_id: &str) -> Result<Vec<GeocoderReverseResult>, GeocoderError> { 75 let mut stmt = self.conn.prepare( 76 r#" 77 SELECT 78 id, 79 name, 80 admin1_id, 81 admin1_name, 82 country_id, 83 country_name, 84 latitude, 85 longitude 86 FROM geonames 87 WHERE country_id = :country_id 88 ORDER BY id ASC 89 "#, 90 )?; 91 collect_mapped_rows( 92 &mut stmt, 93 named_params! { ":country_id": country_id }, 94 map_reverse_row, 95 ) 96 } 97 98 pub fn country_list(&self) -> Result<Vec<GeocoderCountryListResult>, GeocoderError> { 99 let mut stmt = self.conn.prepare( 100 r#" 101 SELECT 102 country_id, 103 country_name, 104 AVG(latitude) AS latitude_c, 105 AVG(longitude) AS longitude_c 106 FROM geonames 107 GROUP BY country_id, country_name 108 ORDER BY country_id ASC 109 "#, 110 )?; 111 collect_mapped_rows(&mut stmt, [], |row| { 112 Ok(GeocoderCountryListResult { 113 country_id: row.get("country_id")?, 114 country: row.get("country_name")?, 115 lat: row.get("latitude_c")?, 116 lng: row.get("longitude_c")?, 117 }) 118 }) 119 } 120 121 pub fn country_center(&self, country_id: &str) -> Result<GeocoderPoint, GeocoderError> { 122 finalize_country_center(country_center_impl(&self.conn, country_id), country_id) 123 } 124 } 125 126 fn query_country_center_row( 127 stmt: &mut rusqlite::Statement<'_>, 128 country_id: &str, 129 ) -> rusqlite::Result<(Option<f64>, Option<f64>)> { 130 stmt.query_row( 131 named_params! { ":country_id": country_id }, 132 map_country_center_row, 133 ) 134 } 135 136 fn map_country_center_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<(Option<f64>, Option<f64>)> { 137 Ok((row.get("latitude_c")?, row.get("longitude_c")?)) 138 } 139 140 #[inline(never)] 141 fn finalize_country_center( 142 result: Result<Option<GeocoderPoint>, GeocoderError>, 143 country_id: &str, 144 ) -> Result<GeocoderPoint, GeocoderError> { 145 let maybe_point = result?; 146 if let Some(point) = maybe_point { 147 return Ok(point); 148 } 149 Err(GeocoderError::CountryCenterNotFound { 150 country_id: country_id.to_owned(), 151 }) 152 } 153 154 fn country_center_impl( 155 conn: &Connection, 156 country_id: &str, 157 ) -> Result<Option<GeocoderPoint>, GeocoderError> { 158 let mut stmt = conn.prepare( 159 r#" 160 SELECT 161 AVG(latitude) AS latitude_c, 162 AVG(longitude) AS longitude_c 163 FROM geonames 164 WHERE country_id = :country_id 165 "#, 166 )?; 167 let (lat, lng) = query_country_center_row(&mut stmt, country_id)?; 168 if let (Some(lat), Some(lng)) = (lat, lng) { 169 return Ok(Some(GeocoderPoint { lat, lng })); 170 } 171 Ok(None) 172 } 173 174 fn collect_mapped_rows<T, P, F>( 175 stmt: &mut rusqlite::Statement<'_>, 176 params: P, 177 map: F, 178 ) -> Result<Vec<T>, GeocoderError> 179 where 180 P: rusqlite::Params, 181 F: FnMut(&rusqlite::Row<'_>) -> rusqlite::Result<T>, 182 { 183 let rows = stmt.query_map(params, map)?; 184 rows.collect::<Result<Vec<_>, _>>() 185 .map_err(GeocoderError::from) 186 } 187 188 fn map_reverse_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<GeocoderReverseResult> { 189 Ok(GeocoderReverseResult { 190 id: row.get("id")?, 191 name: row.get("name")?, 192 admin1_id: row.get("admin1_id")?, 193 admin1_name: row.get("admin1_name")?, 194 country_id: row.get("country_id")?, 195 country_name: row.get("country_name")?, 196 latitude: row.get("latitude")?, 197 longitude: row.get("longitude")?, 198 }) 199 } 200 201 #[cfg(test)] 202 mod tests { 203 use super::*; 204 use rusqlite::Connection; 205 use std::fs; 206 use tempfile::NamedTempFile; 207 208 #[test] 209 fn unit_harness_covers_success_paths() { 210 let geocoder = open_fixture_geocoder(); 211 212 let reverse = geocoder 213 .reverse( 214 GeocoderPoint { 215 lat: 37.7749, 216 lng: -122.4194, 217 }, 218 Some(GeocoderReverseOptions { 219 limit: 2, 220 degree_offset: 10.0, 221 }), 222 ) 223 .expect("reverse query"); 224 assert_eq!(reverse.len(), 2); 225 assert_eq!(reverse[0].id, 1); 226 227 let country = geocoder.country("US").expect("country query"); 228 assert_eq!(country.len(), 3); 229 230 let countries = geocoder.country_list().expect("country list query"); 231 assert_eq!(countries.len(), 2); 232 assert_eq!(countries[0].country_id, "BR"); 233 234 let center = geocoder.country_center("US").expect("country center query"); 235 assert!(approx_eq(center.lat, (37.7749 + 34.0522 + 40.7128) / 3.0)); 236 assert!(approx_eq( 237 center.lng, 238 (-122.4194 + -118.2437 + -74.0060) / 3.0 239 )); 240 } 241 242 #[test] 243 fn unit_harness_covers_open_bytes_and_weighted_reverse_ordering() { 244 let path = build_high_latitude_database(); 245 let bytes = fs::read(&path).expect("read fixture database bytes"); 246 let geocoder = Geocoder::open_bytes(&bytes).expect("open byte-backed geocoder"); 247 248 let results = geocoder 249 .reverse( 250 GeocoderPoint { 251 lat: 75.0, 252 lng: 0.0, 253 }, 254 Some(GeocoderReverseOptions { 255 limit: 2, 256 degree_offset: 1.0, 257 }), 258 ) 259 .expect("reverse query"); 260 261 assert_eq!(results.len(), 2); 262 assert_eq!(results[0].name, "Polar East"); 263 assert_eq!(results[1].name, "Polar North"); 264 } 265 266 #[test] 267 fn unit_harness_covers_open_path_pathbuf_instantiation() { 268 let path = build_fixture_database(); 269 let geocoder = Geocoder::open_path(path.to_path_buf()).expect("open geocoder from pathbuf"); 270 let results = geocoder 271 .country("US") 272 .expect("country query from pathbuf geocoder"); 273 assert_eq!(results.len(), 3); 274 } 275 276 #[test] 277 fn unit_harness_covers_open_path_pathbuf_ref_instantiation() { 278 let temp_path = build_fixture_database(); 279 let path = temp_path.to_path_buf(); 280 let geocoder = Geocoder::open_path(&path).expect("open geocoder from pathbuf ref"); 281 let results = geocoder 282 .country("US") 283 .expect("country query from pathbuf-ref geocoder"); 284 assert_eq!(results.len(), 3); 285 } 286 287 #[test] 288 fn unit_harness_covers_open_path_str_instantiation() { 289 let path = build_fixture_database(); 290 let geocoder = Geocoder::open_path(path.to_str().expect("utf-8 fixture path")) 291 .expect("open geocoder from string path"); 292 let results = geocoder 293 .country("US") 294 .expect("country query from string-path geocoder"); 295 assert_eq!(results.len(), 3); 296 } 297 298 #[test] 299 fn unit_harness_covers_open_path_path_ref_instantiation() { 300 let path = build_fixture_database(); 301 let path_ref = Path::new(path.to_str().expect("utf-8 fixture path")); 302 let geocoder = Geocoder::open_path(path_ref).expect("open geocoder from path ref"); 303 let results = geocoder 304 .country("US") 305 .expect("country query from path-ref geocoder"); 306 assert_eq!(results.len(), 3); 307 } 308 309 #[test] 310 fn unit_harness_covers_open_path_error_path() { 311 let root = tempfile::tempdir().expect("temp dir"); 312 let missing_path = root.path().join("missing").join("fixture.sqlite"); 313 let open_path_err = Geocoder::open_path(&missing_path) 314 .err() 315 .expect("missing parent path should fail"); 316 assert_sqlite_error_contains(open_path_err, ""); 317 } 318 319 #[test] 320 fn unit_harness_covers_missing_schema_errors() { 321 let geocoder = open_empty_geocoder(); 322 323 let reverse_err = geocoder 324 .reverse( 325 GeocoderPoint { 326 lat: 37.7749, 327 lng: -122.4194, 328 }, 329 None, 330 ) 331 .expect_err("reverse should fail without schema"); 332 assert_sqlite_error_contains(reverse_err, "no such"); 333 334 let country_err = geocoder 335 .country("US") 336 .expect_err("country should fail without schema"); 337 assert_sqlite_error_contains(country_err, "no such"); 338 339 let country_list_err = geocoder 340 .country_list() 341 .expect_err("country_list should fail without schema"); 342 assert_sqlite_error_contains(country_list_err, "no such"); 343 344 let country_center_err = geocoder 345 .country_center("US") 346 .expect_err("country_center should fail without schema"); 347 assert_sqlite_error_contains(country_center_err, "no such"); 348 } 349 350 #[test] 351 fn unit_harness_covers_row_mapping_errors() { 352 let reverse_country = open_reverse_country_row_error_geocoder(); 353 let reverse_err = reverse_country 354 .reverse( 355 GeocoderPoint { 356 lat: 37.7749, 357 lng: -122.4194, 358 }, 359 Some(GeocoderReverseOptions { 360 limit: 1, 361 degree_offset: 10.0, 362 }), 363 ) 364 .expect_err("reverse should fail on invalid row mapping"); 365 assert_sqlite_error_contains(reverse_err, "Invalid column type"); 366 367 let country_err = reverse_country 368 .country("US") 369 .expect_err("country should fail on invalid row mapping"); 370 assert_sqlite_error_contains(country_err, "Invalid column type"); 371 372 let country_list = open_country_list_row_error_geocoder(); 373 let country_list_err = country_list 374 .country_list() 375 .expect_err("country_list should fail on null aggregate row"); 376 assert_sqlite_error_contains(country_list_err, "Invalid column type"); 377 } 378 379 #[test] 380 fn unit_harness_covers_query_execution_error_paths() { 381 let reverse_country = geocoder_with_reverse_country_query_execution_error(); 382 let reverse_err = reverse_country 383 .reverse( 384 GeocoderPoint { lat: 1.0, lng: 2.0 }, 385 Some(GeocoderReverseOptions { 386 limit: 1, 387 degree_offset: 1.0, 388 }), 389 ) 390 .expect_err("reverse should fail during query execution"); 391 assert_sqlite_error_contains(reverse_err, "no such function"); 392 393 let country_err = reverse_country 394 .country("US") 395 .expect_err("country should fail during query execution"); 396 assert_sqlite_error_contains(country_err, "no such function"); 397 398 let country_list_err = geocoder_with_country_list_query_execution_error() 399 .country_list() 400 .expect_err("country_list should fail during query execution"); 401 assert_sqlite_error_contains(country_list_err, "no such function"); 402 403 let country_center_err = geocoder_with_country_center_query_execution_error() 404 .country_center("US") 405 .expect_err("country_center should fail during query execution"); 406 assert_sqlite_error_contains(country_center_err, "no such function"); 407 } 408 409 #[test] 410 fn unit_harness_covers_country_list_field_error_paths() { 411 let country_id_err = 412 geocoder_with_country_list_sql_row("1", "'United States'", "37.0", "1.0") 413 .country_list() 414 .expect_err("country_id type mismatch should fail"); 415 assert_sqlite_error_contains(country_id_err, "Invalid column type"); 416 417 let country_name_err = geocoder_with_country_list_sql_row("'US'", "1", "37.0", "1.0") 418 .country_list() 419 .expect_err("country_name type mismatch should fail"); 420 assert_sqlite_error_contains(country_name_err, "Invalid column type"); 421 422 let longitude_err = 423 geocoder_with_country_list_sql_row("'US'", "'United States'", "37.0", "NULL") 424 .country_list() 425 .expect_err("longitude type mismatch should fail"); 426 assert_sqlite_error_contains(longitude_err, "Invalid column type"); 427 } 428 429 #[test] 430 fn unit_harness_covers_country_center_row_error_paths() { 431 let latitude_err = map_country_center_row_error("'bad'", "1.0"); 432 assert_sqlite_error_contains(GeocoderError::from(latitude_err), "Invalid column type"); 433 434 let longitude_err = map_country_center_row_error("1.0", "'bad'"); 435 assert_sqlite_error_contains(GeocoderError::from(longitude_err), "Invalid column type"); 436 } 437 438 #[test] 439 fn unit_harness_covers_reverse_row_field_error_paths() { 440 for err in [ 441 map_reverse_row_error( 442 "'bad'", 443 "'name'", 444 "1", 445 "'admin'", 446 "'US'", 447 "'United States'", 448 "1.0", 449 "2.0", 450 ), 451 map_reverse_row_error( 452 "1", 453 "'name'", 454 "'bad'", 455 "'admin'", 456 "'US'", 457 "'United States'", 458 "1.0", 459 "2.0", 460 ), 461 map_reverse_row_error( 462 "1", 463 "'name'", 464 "1", 465 "1", 466 "'US'", 467 "'United States'", 468 "1.0", 469 "2.0", 470 ), 471 map_reverse_row_error( 472 "1", 473 "'name'", 474 "1", 475 "'admin'", 476 "1", 477 "'United States'", 478 "1.0", 479 "2.0", 480 ), 481 map_reverse_row_error("1", "'name'", "1", "'admin'", "'US'", "1", "1.0", "2.0"), 482 map_reverse_row_error( 483 "1", 484 "'name'", 485 "1", 486 "'admin'", 487 "'US'", 488 "'United States'", 489 "'bad'", 490 "2.0", 491 ), 492 map_reverse_row_error( 493 "1", 494 "'name'", 495 "1", 496 "'admin'", 497 "'US'", 498 "'United States'", 499 "1.0", 500 "'bad'", 501 ), 502 ] { 503 assert_sqlite_error_contains(GeocoderError::from(err), "Invalid column type"); 504 } 505 } 506 507 #[test] 508 fn unit_harness_covers_country_center_not_found() { 509 let geocoder = open_fixture_geocoder(); 510 let err = geocoder 511 .country_center("ZZ") 512 .expect_err("missing country should return not found"); 513 assert_country_center_not_found(err, "ZZ"); 514 } 515 516 #[test] 517 fn unit_harness_covers_helper_panic_paths() { 518 let sqlite_panic = std::panic::catch_unwind(|| { 519 assert_sqlite_error_contains( 520 GeocoderError::CountryCenterNotFound { 521 country_id: "US".to_owned(), 522 }, 523 "no such", 524 ); 525 }); 526 assert!(sqlite_panic.is_err()); 527 528 let country_center_panic = std::panic::catch_unwind(|| { 529 let mismatch_err = GeocoderError::Sqlite(rusqlite::Error::InvalidQuery); 530 assert_country_center_not_found(mismatch_err, "US"); 531 }); 532 assert!(country_center_panic.is_err()); 533 } 534 535 fn open_fixture_geocoder() -> Geocoder { 536 let path = build_fixture_database(); 537 Geocoder::open_path(&path).expect("open geocoder") 538 } 539 540 fn open_empty_geocoder() -> Geocoder { 541 let temp = NamedTempFile::new().expect("temp db"); 542 let path = temp.into_temp_path(); 543 Geocoder::open_path(&path).expect("open empty geocoder") 544 } 545 546 fn open_reverse_country_row_error_geocoder() -> Geocoder { 547 let temp = NamedTempFile::new().expect("temp db"); 548 let path = temp.into_temp_path(); 549 seed_reverse_country_row_error_database(path.to_str().expect("utf-8 temp path")); 550 Geocoder::open_path(&path).expect("open invalid row geocoder") 551 } 552 553 fn open_country_list_row_error_geocoder() -> Geocoder { 554 let temp = NamedTempFile::new().expect("temp db"); 555 let path = temp.into_temp_path(); 556 seed_country_list_row_error_database(path.to_str().expect("utf-8 temp path")); 557 Geocoder::open_path(&path).expect("open aggregate error geocoder") 558 } 559 560 fn build_fixture_database() -> tempfile::TempPath { 561 let temp = NamedTempFile::new().expect("temp db"); 562 let path = temp.into_temp_path(); 563 seed_fixture_database(path.to_str().expect("utf-8 temp path")); 564 path 565 } 566 567 fn build_high_latitude_database() -> tempfile::TempPath { 568 let temp = NamedTempFile::new().expect("temp db"); 569 let path = temp.into_temp_path(); 570 seed_high_latitude_database(path.to_str().expect("utf-8 temp path")); 571 path 572 } 573 574 fn geocoder_with_reverse_country_query_execution_error() -> Geocoder { 575 let conn = Connection::open_in_memory().expect("open in-memory query error db"); 576 conn.execute_batch( 577 r#" 578 CREATE VIEW geonames AS 579 SELECT 580 1 AS id, 581 missing_reverse_name() AS name, 582 1 AS admin1_id, 583 'Admin' AS admin1_name, 584 'US' AS country_id, 585 'United States' AS country_name, 586 1.0 AS latitude, 587 2.0 AS longitude; 588 CREATE TABLE coordinates( 589 feature_id INTEGER, 590 latitude REAL, 591 longitude REAL 592 ); 593 INSERT INTO coordinates (feature_id, latitude, longitude) VALUES (1, 1.0, 2.0); 594 "#, 595 ) 596 .expect("create reverse/country execution error schema"); 597 Geocoder { 598 conn, 599 _temp_path: None, 600 } 601 } 602 603 fn geocoder_with_country_list_query_execution_error() -> Geocoder { 604 let conn = 605 Connection::open_in_memory().expect("open in-memory country_list query error db"); 606 conn.execute_batch( 607 r#" 608 CREATE VIEW geonames AS 609 SELECT 610 'US' AS country_id, 611 missing_country_name() AS country_name, 612 1.0 AS latitude, 613 2.0 AS longitude; 614 "#, 615 ) 616 .expect("create country_list execution error schema"); 617 Geocoder { 618 conn, 619 _temp_path: None, 620 } 621 } 622 623 fn geocoder_with_country_center_query_execution_error() -> Geocoder { 624 let conn = 625 Connection::open_in_memory().expect("open in-memory country_center query error db"); 626 conn.execute_batch( 627 r#" 628 CREATE VIEW geonames AS 629 SELECT 630 'US' AS country_id, 631 missing_latitude() AS latitude, 632 2.0 AS longitude; 633 "#, 634 ) 635 .expect("create country_center execution error schema"); 636 Geocoder { 637 conn, 638 _temp_path: None, 639 } 640 } 641 642 fn geocoder_with_country_list_sql_row( 643 country_id_sql: &str, 644 country_name_sql: &str, 645 latitude_sql: &str, 646 longitude_sql: &str, 647 ) -> Geocoder { 648 let conn = 649 Connection::open_in_memory().expect("open in-memory country_list field error db"); 650 conn.execute_batch(&format!( 651 r#" 652 CREATE TABLE geonames( 653 country_id, 654 country_name, 655 latitude, 656 longitude 657 ); 658 INSERT INTO geonames (country_id, country_name, latitude, longitude) 659 VALUES ({country_id_sql}, {country_name_sql}, {latitude_sql}, {longitude_sql}); 660 "#, 661 )) 662 .expect("create country_list field error schema"); 663 Geocoder { 664 conn, 665 _temp_path: None, 666 } 667 } 668 669 fn map_country_center_row_error(latitude_sql: &str, longitude_sql: &str) -> rusqlite::Error { 670 let conn = 671 Connection::open_in_memory().expect("open in-memory country center row error db"); 672 conn.query_row( 673 &format!("SELECT {latitude_sql} AS latitude_c, {longitude_sql} AS longitude_c"), 674 [], 675 map_country_center_row, 676 ) 677 .expect_err("country center row decode should fail") 678 } 679 680 fn map_reverse_row_error( 681 id_sql: &str, 682 name_sql: &str, 683 admin1_id_sql: &str, 684 admin1_name_sql: &str, 685 country_id_sql: &str, 686 country_name_sql: &str, 687 latitude_sql: &str, 688 longitude_sql: &str, 689 ) -> rusqlite::Error { 690 let conn = Connection::open_in_memory().expect("open in-memory reverse row error db"); 691 conn.query_row( 692 &format!( 693 r#" 694 SELECT 695 {id_sql} AS id, 696 {name_sql} AS name, 697 {admin1_id_sql} AS admin1_id, 698 {admin1_name_sql} AS admin1_name, 699 {country_id_sql} AS country_id, 700 {country_name_sql} AS country_name, 701 {latitude_sql} AS latitude, 702 {longitude_sql} AS longitude 703 "#, 704 ), 705 [], 706 map_reverse_row, 707 ) 708 .expect_err("reverse row decode should fail") 709 } 710 711 fn seed_fixture_database(path: &str) { 712 let conn = Connection::open(path).expect("open fixture database"); 713 seed_schema(&conn); 714 715 insert_country(&conn, "US", "United States"); 716 insert_country(&conn, "BR", "Brazil"); 717 718 insert_admin1(&conn, "US", 6, "California"); 719 insert_admin1(&conn, "US", 36, "New York"); 720 insert_admin1(&conn, "BR", 27, "Sao Paulo"); 721 722 insert_feature(&conn, 1, "San Francisco", "US", 6, 37.7749, -122.4194); 723 insert_feature(&conn, 2, "Los Angeles", "US", 6, 34.0522, -118.2437); 724 insert_feature(&conn, 3, "New York City", "US", 36, 40.7128, -74.0060); 725 insert_feature(&conn, 4, "Sao Paulo", "BR", 27, -23.5505, -46.6333); 726 } 727 728 fn seed_high_latitude_database(path: &str) { 729 let conn = Connection::open(path).expect("open fixture database"); 730 seed_schema(&conn); 731 732 insert_country(&conn, "NO", "Norway"); 733 insert_admin1(&conn, "NO", 1, "Nord"); 734 735 insert_feature(&conn, 1, "Polar East", "NO", 1, 75.02, 0.10); 736 insert_feature(&conn, 2, "Polar North", "NO", 1, 75.05, 0.05); 737 } 738 739 fn seed_reverse_country_row_error_database(path: &str) { 740 let conn = Connection::open(path).expect("open invalid row fixture database"); 741 conn.execute_batch( 742 r#" 743 CREATE TABLE geonames( 744 id INTEGER, 745 name TEXT, 746 admin1_id INTEGER, 747 admin1_name TEXT, 748 country_id TEXT, 749 country_name TEXT, 750 latitude REAL, 751 longitude REAL 752 ); 753 CREATE TABLE coordinates( 754 feature_id INTEGER, 755 latitude REAL, 756 longitude REAL 757 ); 758 "#, 759 ) 760 .expect("create invalid row schema"); 761 conn.execute( 762 "INSERT INTO geonames (id, name, admin1_id, admin1_name, country_id, country_name, latitude, longitude) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", 763 rusqlite::params![1_i64, Option::<String>::None, Option::<i64>::None, Option::<String>::None, "US", "United States", 37.7749_f64, -122.4194_f64], 764 ) 765 .expect("insert invalid reverse/country row"); 766 conn.execute( 767 "INSERT INTO coordinates (feature_id, latitude, longitude) VALUES (?1, ?2, ?3)", 768 (1_i64, 37.7749_f64, -122.4194_f64), 769 ) 770 .expect("insert invalid reverse/country coordinate"); 771 } 772 773 fn seed_country_list_row_error_database(path: &str) { 774 let conn = Connection::open(path).expect("open aggregate error fixture database"); 775 conn.execute_batch( 776 r#" 777 CREATE TABLE geonames( 778 country_id TEXT, 779 country_name TEXT, 780 latitude REAL, 781 longitude REAL 782 ); 783 "#, 784 ) 785 .expect("create aggregate error schema"); 786 conn.execute( 787 "INSERT INTO geonames (country_id, country_name, latitude, longitude) VALUES (?1, ?2, ?3, ?4)", 788 rusqlite::params!["US", "United States", Option::<f64>::None, Option::<f64>::None], 789 ) 790 .expect("insert aggregate error row"); 791 } 792 793 fn seed_schema(conn: &Connection) { 794 conn.execute_batch( 795 r#" 796 CREATE TABLE countries( 797 id TEXT, 798 name TEXT, 799 PRIMARY KEY (id) 800 ); 801 CREATE TABLE admin1( 802 country_id TEXT, 803 id INTEGER, 804 name TEXT, 805 PRIMARY KEY (country_id, id) 806 ); 807 CREATE TABLE features( 808 id INTEGER, 809 name TEXT, 810 country_id TEXT, 811 admin1_id INTEGER, 812 PRIMARY KEY (id) 813 ); 814 CREATE TABLE coordinates( 815 feature_id INTEGER, 816 latitude REAL, 817 longitude REAL, 818 PRIMARY KEY (feature_id) 819 ); 820 CREATE INDEX coordinates_lat_lng ON coordinates (latitude, longitude); 821 CREATE VIEW geonames AS 822 SELECT 823 features.id, 824 features.name, 825 admin1.id AS admin1_id, 826 admin1.name AS admin1_name, 827 countries.id AS country_id, 828 countries.name AS country_name, 829 coordinates.latitude AS latitude, 830 coordinates.longitude AS longitude 831 FROM features 832 LEFT JOIN countries ON features.country_id = countries.id 833 LEFT JOIN admin1 ON features.country_id = admin1.country_id AND features.admin1_id = admin1.id 834 JOIN coordinates ON features.id = coordinates.feature_id; 835 "#, 836 ) 837 .expect("create fixture schema"); 838 } 839 840 fn insert_country(conn: &Connection, id: &str, name: &str) { 841 conn.execute( 842 "INSERT INTO countries (id, name) VALUES (?1, ?2)", 843 (id, name), 844 ) 845 .expect("insert country"); 846 } 847 848 fn insert_admin1(conn: &Connection, country_id: &str, id: i64, name: &str) { 849 conn.execute( 850 "INSERT INTO admin1 (country_id, id, name) VALUES (?1, ?2, ?3)", 851 (country_id, id, name), 852 ) 853 .expect("insert admin1"); 854 } 855 856 fn insert_feature( 857 conn: &Connection, 858 id: i64, 859 name: &str, 860 country_id: &str, 861 admin1_id: i64, 862 latitude: f64, 863 longitude: f64, 864 ) { 865 conn.execute( 866 "INSERT INTO features (id, name, country_id, admin1_id) VALUES (?1, ?2, ?3, ?4)", 867 (id, name, country_id, admin1_id), 868 ) 869 .expect("insert feature"); 870 conn.execute( 871 "INSERT INTO coordinates (feature_id, latitude, longitude) VALUES (?1, ?2, ?3)", 872 (id, latitude, longitude), 873 ) 874 .expect("insert coordinate"); 875 } 876 877 fn approx_eq(left: f64, right: f64) -> bool { 878 (left - right).abs() < 0.000_001 879 } 880 881 fn assert_sqlite_error_contains(err: GeocoderError, needle: &str) { 882 match err { 883 GeocoderError::Sqlite(inner) => assert!( 884 inner.to_string().contains(needle), 885 "expected sqlite error containing {needle:?}, got {inner}" 886 ), 887 other => panic!("expected sqlite error, got {other}"), 888 } 889 } 890 891 fn assert_country_center_not_found(err: GeocoderError, country_id: &str) { 892 match err { 893 GeocoderError::CountryCenterNotFound { country_id: actual } => { 894 assert_eq!(actual, country_id); 895 } 896 other => panic!("expected CountryCenterNotFound, got {other}"), 897 } 898 } 899 }