use rusqlite::{functions::FunctionFlags, Connection};
#[cfg(feature = "raw-sqlite")]
use std::sync::MutexGuard;
use std::sync::{Mutex, OnceLock};
use crate::types::{
haversine_km, location_from_village, DataInfo, Location, LookupResult, PrefixResult, Village,
};
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>,
}
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),
})
}
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>> {
locate(&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),
}
}
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, 20);
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, 100);
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| {
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,
})
})?;
rows.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Error::from)
}
fn by_code(conn: &Connection, code: &str) -> Result<Option<Village>> {
let mut stmt = conn.prepare_cached(
"SELECT kode, nama, kecamatan, kota, provinsi, lat, lon
FROM locations
WHERE kode = ?1",
)?;
let mut rows = stmt.query_map(rusqlite::params![code], |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: None,
})
})?;
match rows.next() {
Some(Ok(v)) => Ok(Some(v)),
Some(Err(e)) => Err(Error::from(e)),
None => Ok(None),
}
}
fn by_code_prefix(
conn: &Connection,
prefix: &str,
limit: usize,
offset: usize,
) -> Result<PrefixResult> {
let limit = limit.clamp(1, 1000);
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],
|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: None,
})
},
)?;
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], |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: None,
})
})?;
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(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))
}