lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

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 }