use geopackage_core::ident::quote;
use rusqlite::OptionalExtension;
use crate::{BoundingBox, Error, Layer, Result};
impl Layer<'_> {
pub fn extent(&self) -> Result<Option<BoundingBox>> {
if self.geometry_column().is_none() {
return Err(Error::NoGeometryColumn {
table_name: self.table_name().to_owned(),
});
}
if let Some(stored) = self.stored_extent()? {
return Ok(Some(stored));
}
self.measure_and_record()
}
fn measure_and_record(&self) -> Result<Option<BoundingBox>> {
let conn = self.gpkg().connection();
if conn.is_readonly(rusqlite::MAIN_DB)? {
return self.measure_extent(conn);
}
if !conn.is_autocommit() {
let measured = self.measure_extent(conn)?;
let recorded = self.record_extent(conn, measured);
return self.finish_recording(measured, recorded);
}
let tx = conn.unchecked_transaction()?;
let measured = self.measure_extent(&tx)?;
let recorded = self.record_extent(&tx, measured).and_then(|()| tx.commit());
self.finish_recording(measured, recorded)
}
fn record_extent(
&self,
conn: &rusqlite::Connection,
measured: Option<BoundingBox>,
) -> rusqlite::Result<()> {
match measured {
Some(bbox) => conn.execute(
"UPDATE gpkg_contents SET min_x = ?1, min_y = ?2, max_x = ?3, max_y = ?4 \
WHERE table_name = ?5",
rusqlite::params![
bbox.min_x,
bbox.min_y,
bbox.max_x,
bbox.max_y,
self.table_name()
],
),
None => conn.execute(
"UPDATE gpkg_contents \
SET min_x = NULL, min_y = NULL, max_x = NULL, max_y = NULL \
WHERE table_name = ?1 AND (min_x IS NOT NULL OR min_y IS NOT NULL \
OR max_x IS NOT NULL OR max_y IS NOT NULL)",
[self.table_name()],
),
}
.map(|_| ())
}
fn finish_recording(
&self,
measured: Option<BoundingBox>,
recorded: rusqlite::Result<()>,
) -> Result<Option<BoundingBox>> {
match recorded {
Ok(()) => Ok(measured),
Err(source) if is_lock_contention(&source) => Ok(measured),
Err(source) => Err(Error::ExtentPersist {
table_name: self.table_name().to_owned(),
extent: measured,
source: Box::new(source),
}),
}
}
pub fn recompute_extent(&self) -> Result<Option<BoundingBox>> {
let conn = self.gpkg().connection().unchecked_transaction()?;
let measured = self.measure_extent(&conn)?;
match measured {
Some(bbox) => conn.execute(
"UPDATE gpkg_contents SET min_x = ?1, min_y = ?2, max_x = ?3, max_y = ?4 \
WHERE table_name = ?5",
rusqlite::params![
bbox.min_x,
bbox.min_y,
bbox.max_x,
bbox.max_y,
self.table_name()
],
)?,
None => conn.execute(
"UPDATE gpkg_contents \
SET min_x = NULL, min_y = NULL, max_x = NULL, max_y = NULL \
WHERE table_name = ?1",
[self.table_name()],
)?,
};
conn.commit()?;
Ok(measured)
}
pub(crate) fn stored_extent(&self) -> Result<Option<BoundingBox>> {
let bounds: Option<[Option<f64>; 4]> = self
.gpkg()
.connection()
.query_row(
"SELECT min_x, min_y, max_x, max_y FROM gpkg_contents WHERE table_name = ?1",
[self.table_name()],
|row| Ok([row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?]),
)
.optional()?;
let Some([Some(min_x), Some(min_y), Some(max_x), Some(max_y)]) = bounds else {
return Ok(None);
};
if min_x > max_x || min_y > max_y {
return Ok(None);
}
Ok(Some(BoundingBox::new(min_x, min_y, max_x, max_y)))
}
fn measure_extent(&self, conn: &rusqlite::Connection) -> Result<Option<BoundingBox>> {
let geometry = self
.geometry_column()
.ok_or_else(|| Error::NoGeometryColumn {
table_name: self.table_name().to_owned(),
})?;
let column = quote(&geometry.column_name)?;
let sql = format!(
"SELECT min(ST_MinX({column})), min(ST_MinY({column})), \
max(ST_MaxX({column})), max(ST_MaxY({column})) FROM {}",
quote(self.table_name())?
);
let bounds: [Option<f64>; 4] = conn.query_row(&sql, [], |row| {
Ok([row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?])
})?;
let [Some(min_x), Some(min_y), Some(max_x), Some(max_y)] = bounds else {
return Ok(None);
};
Ok(Some(BoundingBox::new(min_x, min_y, max_x, max_y)))
}
pub(crate) fn has_rows(&self) -> Result<bool> {
let sql = format!(
"SELECT EXISTS(SELECT 1 FROM {} LIMIT 1)",
quote(self.table_name())?
);
Ok(self.gpkg().connection().query_row(&sql, [], |row| {
row.get::<_, i64>(0).map(|exists| exists != 0)
})?)
}
}
fn is_lock_contention(error: &rusqlite::Error) -> bool {
matches!(
error.sqlite_error_code(),
Some(rusqlite::ErrorCode::DatabaseBusy)
)
}