use rusqlite::{functions::FunctionFlags, Connection};
#[cfg(feature = "raw-sqlite")]
use std::sync::MutexGuard;
use std::sync::{Mutex, OnceLock};
use crate::types::{
deserialize_vertices, haversine_km, location_from_village, point_in_polygon, DataInfo,
LocateMethod, Location, LookupResult, PrefixResult, Village, CODE_PREFIX_MAX_LIMIT,
NEAREST_MAX_LIMIT, SEARCH_MAX_LIMIT,
};
const DB_BYTES: &[u8] = include_bytes!(env!("LOCATION_DB_PATH"));
#[derive(Debug)]
pub struct Error {
inner: rusqlite::Error,
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.inner.fmt(f)
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.inner)
}
}
impl From<rusqlite::Error> for Error {
fn from(e: rusqlite::Error) -> Self {
Error { inner: e }
}
}
impl serde::Serialize for Error {
fn serialize<S: serde::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
s.serialize_str(&self.to_string())
}
}
pub type Result<T> = std::result::Result<T, Error>;
pub struct Database {
conn: Mutex<Connection>,
poly_conn: Option<Mutex<Connection>>,
}
impl Database {
pub fn open() -> Result<Self> {
let mut conn = Connection::open_in_memory()?;
conn.execute_batch("PRAGMA journal_mode = OFF")?;
conn.deserialize_bytes("main", DB_BYTES)?;
conn.create_scalar_function(
"haversine_km",
4,
FunctionFlags::SQLITE_UTF8 | FunctionFlags::SQLITE_DETERMINISTIC,
move |ctx| {
Ok(haversine_km(
ctx.get::<f64>(0)?,
ctx.get::<f64>(1)?,
ctx.get::<f64>(2)?,
ctx.get::<f64>(3)?,
))
},
)?;
Ok(Database {
conn: Mutex::new(conn),
poly_conn: None,
})
}
pub fn open_with_polygons(poly_path: &str) -> Result<Self> {
let mut db = Self::open()?;
let poly_conn = Connection::open(poly_path)?;
db.poly_conn = Some(Mutex::new(poly_conn));
Ok(db)
}
pub fn has_polygons(&self) -> bool {
self.poly_conn.is_some()
}
pub fn find_nearest(&self, lat: f64, lon: f64, limit: usize) -> Result<Vec<Village>> {
nearest(&self.conn.lock().unwrap(), lat, lon, limit)
}
pub fn find_by_name(&self, query: &str, limit: usize) -> Result<Vec<Village>> {
search(&self.conn.lock().unwrap(), query, limit)
}
pub fn find_by_name_unique(&self, query: &str) -> Result<LookupResult> {
search_unique(&self.conn.lock().unwrap(), query)
}
pub fn find_by_code(&self, code: &str) -> Result<Option<Village>> {
by_code(&self.conn.lock().unwrap(), code)
}
pub fn find_by_code_prefix(
&self,
prefix: &str,
limit: usize,
offset: usize,
) -> Result<PrefixResult> {
by_code_prefix(&self.conn.lock().unwrap(), prefix, limit, offset)
}
pub fn locate(&self, lat: f64, lon: f64) -> Result<Option<Location>> {
if let Some(poly_conn) = &self.poly_conn {
let candidates = {
let poly = poly_conn.lock().unwrap();
query_polygon_candidates(&poly, lat, lon)?
};
for (village_id, rings) in &candidates {
let exteriors: Vec<&[(f64, f64)]> = rings
.iter()
.filter(|(rt, _)| rt == "exterior")
.map(|(_, v)| v.as_slice())
.collect();
let interiors: Vec<&[(f64, f64)]> = rings
.iter()
.filter(|(rt, _)| rt == "interior")
.map(|(_, v)| v.as_slice())
.collect();
for exterior in &exteriors {
if point_in_polygon(lat, lon, exterior, &interiors) {
let conn = self.conn.lock().unwrap();
let village = by_id(&conn, *village_id)?;
let Some(village) = village else {
continue;
};
let dist_km = haversine_km(lat, lon, village.lat, village.lon);
return Ok(location_from_village(
&village,
dist_km,
LocateMethod::Contained,
));
}
}
}
return locate_nearest(&self.conn.lock().unwrap(), lat, lon);
}
locate_nearest(&self.conn.lock().unwrap(), lat, lon)
}
pub fn data_info(&self) -> DataInfo {
data_info_from_conn(&self.conn.lock().unwrap())
}
pub fn village_count(&self) -> Result<u32> {
let count: i64 =
self.conn
.lock()
.unwrap()
.query_row("SELECT COUNT(*) FROM locations", [], |row| row.get(0))?;
Ok(count as u32)
}
}
#[cfg(feature = "raw-sqlite")]
impl Database {
pub fn conn_guard(&self) -> MutexGuard<'_, Connection> {
self.conn.lock().unwrap()
}
}
fn query_meta(conn: &Connection, key: &str) -> Option<String> {
conn.query_row("SELECT value FROM db_meta WHERE key = ?1", [key], |row| {
row.get(0)
})
.ok()
}
fn data_info_from_conn(conn: &Connection) -> DataInfo {
DataInfo {
source: query_meta(conn, "source").unwrap_or_else(|| "unknown".to_string()),
decree: query_meta(conn, "decree").unwrap_or_else(|| "unknown".to_string()),
village_count: query_meta(conn, "village_count")
.and_then(|s| s.parse().ok())
.unwrap_or(0),
build_date: query_meta(conn, "build_date")
.and_then(|s| s.parse().ok())
.unwrap_or(0),
}
}
fn village_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<Village> {
Ok(Village {
code: row.get(0)?,
name: row.get(1)?,
district: row.get(2)?,
city: row.get(3)?,
province: row.get(4)?,
lat: row.get(5)?,
lon: row.get(6)?,
dist_km: None,
})
}
fn village_by_field<P: rusqlite::types::ToSql>(
conn: &Connection,
sql: &str,
param: P,
) -> Result<Option<Village>> {
let mut stmt = conn.prepare_cached(sql)?;
let mut rows = stmt.query_map(rusqlite::params![param], village_from_row)?;
match rows.next() {
Some(Ok(v)) => Ok(Some(v)),
Some(Err(e)) => Err(Error::from(e)),
None => Ok(None),
}
}
static CACHED_DATA_INFO: OnceLock<DataInfo> = OnceLock::new();
pub(crate) fn cached_data_info() -> &'static DataInfo {
CACHED_DATA_INFO.get_or_init(|| {
let db = Database::open().expect("failed to open embedded database for metadata");
db.data_info()
})
}
fn nearest(conn: &Connection, lat: f64, lon: f64, limit: usize) -> Result<Vec<Village>> {
let limit = limit.clamp(1, NEAREST_MAX_LIMIT);
let deltas: [f64; 10] = [0.01, 0.05, 0.1, 0.5, 1.0, 2.0, 5.0, 15.0, 45.0, 180.0];
for &delta in &deltas {
let sql = "
SELECT l.kode, l.nama, l.kecamatan, l.kota, l.provinsi, l.lat, l.lon,
haversine_km(?1, ?2, l.lat, l.lon) AS dist
FROM locations l
JOIN geo_rtree r ON l.id = r.id
WHERE r.min_lon <= ?4 AND r.max_lon >= ?3
AND r.min_lat <= ?6 AND r.max_lat >= ?5
ORDER BY dist
LIMIT ?7
";
let mut stmt = conn.prepare_cached(sql)?;
let rows = stmt.query_map(
rusqlite::params![
lat,
lon,
lon - delta,
lon + delta,
lat - delta,
lat + delta,
limit as i64
],
|row| {
Ok(Village {
code: row.get(0)?,
name: row.get(1)?,
district: row.get(2)?,
city: row.get(3)?,
province: row.get(4)?,
lat: row.get(5)?,
lon: row.get(6)?,
dist_km: Some(row.get(7)?),
})
},
)?;
let results: Vec<Village> = rows
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Error::from)?;
if results.len() >= limit {
return Ok(results);
}
}
Ok(vec![])
}
fn search(conn: &Connection, query: &str, limit: usize) -> Result<Vec<Village>> {
let limit = limit.clamp(1, SEARCH_MAX_LIMIT);
let sql = "
SELECT l.kode, l.nama, l.kecamatan, l.kota, l.provinsi, l.lat, l.lon
FROM locations_fts f
JOIN locations l ON f.rowid = l.id
WHERE locations_fts MATCH ?1
ORDER BY rank
LIMIT ?2
";
let mut stmt = conn.prepare_cached(sql)?;
let rows = stmt.query_map(rusqlite::params![query, limit as i64], |row| {
village_from_row(row)
})?;
rows.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Error::from)
}
fn by_code(conn: &Connection, code: &str) -> Result<Option<Village>> {
village_by_field(
conn,
"SELECT kode, nama, kecamatan, kota, provinsi, lat, lon
FROM locations WHERE kode = ?1",
code,
)
}
fn by_code_prefix(
conn: &Connection,
prefix: &str,
limit: usize,
offset: usize,
) -> Result<PrefixResult> {
let limit = limit.clamp(1, CODE_PREFIX_MAX_LIMIT);
let pattern = format!("{}%", prefix);
let total_i64: i64 = conn.query_row(
"SELECT COUNT(*) FROM locations WHERE kode LIKE ?1",
[&pattern],
|row| row.get(0),
)?;
let total = total_i64 as usize;
let mut stmt = conn.prepare_cached(
"SELECT kode, nama, kecamatan, kota, provinsi, lat, lon
FROM locations
WHERE kode LIKE ?1
ORDER BY kode
LIMIT ?2
OFFSET ?3",
)?;
let rows = stmt.query_map(
rusqlite::params![pattern, limit as i64, offset as i64],
village_from_row,
)?;
let villages: Vec<Village> = rows
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Error::from)?;
let has_more = offset + villages.len() < total;
Ok(PrefixResult {
villages,
total,
has_more,
})
}
fn search_unique(conn: &Connection, query: &str) -> Result<LookupResult> {
let mut stmt = conn.prepare_cached(
"SELECT l.kode, l.nama, l.kecamatan, l.kota, l.provinsi, l.lat, l.lon
FROM locations_fts f
JOIN locations l ON f.rowid = l.id
WHERE locations_fts MATCH ?1
ORDER BY rank
LIMIT 20",
)?;
let rows = stmt.query_map(rusqlite::params![query], village_from_row)?;
let results: Vec<_> = rows
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Error::from)?;
Ok(match results.len() {
0 => LookupResult::NotFound,
1 => LookupResult::Found(results.into_iter().next().unwrap()),
_ => LookupResult::Ambiguous(results),
})
}
fn locate_nearest(conn: &Connection, lat: f64, lon: f64) -> Result<Option<Location>> {
let mut results = nearest(conn, lat, lon, 1)?;
let village = match results.pop() {
Some(v) => v,
None => return Ok(None),
};
let dist_km = village.dist_km.unwrap_or(0.0);
Ok(location_from_village(
&village,
dist_km,
LocateMethod::Nearest,
))
}
type VillageRingMap = std::collections::HashMap<i64, Vec<(String, Vec<(f64, f64)>)>>;
fn query_polygon_candidates(poly_conn: &Connection, lat: f64, lon: f64) -> Result<VillageRingMap> {
let sql = "
SELECT vp.village_id, vp.ring_type, vp.vertices
FROM village_polygons vp
WHERE vp.min_lon <= ?2 AND vp.max_lon >= ?1
AND vp.min_lat <= ?4 AND vp.max_lat >= ?3
";
let mut stmt = poly_conn.prepare_cached(sql)?;
let rows = stmt.query_map(rusqlite::params![lon, lon, lat, lat], |row| {
let village_id: i64 = row.get(0)?;
let ring_type: String = row.get(1)?;
let vertices_blob: Vec<u8> = row.get(2)?;
Ok((village_id, ring_type, vertices_blob))
})?;
let mut village_rings = VillageRingMap::new();
for row in rows {
let (village_id, ring_type, blob) = row?;
let vertices = deserialize_vertices(&blob);
village_rings
.entry(village_id)
.or_default()
.push((ring_type, vertices));
}
Ok(village_rings)
}
fn by_id(conn: &Connection, id: i64) -> Result<Option<Village>> {
village_by_field(
conn,
"SELECT kode, nama, kecamatan, kota, provinsi, lat, lon FROM locations WHERE id = ?1",
id,
)
}