use std::collections::HashMap;
use geopackage_core::ident::quote;
use geopackage_core::triggers;
use rusqlite::Connection;
use crate::Result;
pub const DEFAULT_BULK_THRESHOLD: usize = 10_000;
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub struct BulkIndexOptions {
pub bulk_threshold: usize,
}
impl Default for BulkIndexOptions {
fn default() -> Self {
Self {
bulk_threshold: DEFAULT_BULK_THRESHOLD,
}
}
}
impl BulkIndexOptions {
pub fn with_threshold(bulk_threshold: usize) -> Self {
Self { bulk_threshold }
}
pub fn always_bulk() -> Self {
Self { bulk_threshold: 0 }
}
pub fn never_bulk() -> Self {
Self {
bulk_threshold: usize::MAX,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum BuildPath {
Triggered,
Bulk,
TriggeredFallback,
}
pub(crate) type ScratchTamper = fn(&ScratchDb<'_>) -> Result<()>;
pub(crate) fn no_tamper(_: &ScratchDb<'_>) -> Result<()> {
Ok(())
}
const SCRATCH_ALIAS: &str = "gpkg_bulk_scratch";
const SCRATCH_RTREE: &str = "gpkg_bulk_rtree";
pub(crate) struct ScratchDb<'c> {
conn: &'c Connection,
}
impl<'c> ScratchDb<'c> {
fn attach(conn: &'c Connection) -> Result<Self> {
best_effort(conn, &format!("DETACH DATABASE {}", quote(SCRATCH_ALIAS)?));
conn.execute_batch(&format!(
"ATTACH DATABASE ':memory:' AS {}",
quote(SCRATCH_ALIAS)?
))?;
conn.execute_batch(&format!(
"CREATE VIRTUAL TABLE {} USING rtree(id, minx, maxx, miny, maxy)",
self_scratch_rtree()?
))?;
Ok(Self { conn })
}
fn build(&self, rows: &[(i64, [f64; 4])]) -> Result<()> {
let sql = format!(
"INSERT INTO {} VALUES (?1, ?2, ?3, ?4, ?5)",
self_scratch_rtree()?
);
let mut stmt = self.conn.prepare(&sql)?;
for (id, [min_x, max_x, min_y, max_y]) in rows {
stmt.execute(rusqlite::params![id, min_x, max_x, min_y, max_y])?;
}
Ok(())
}
#[cfg(test)]
pub(crate) fn insert_scratch_row(
&self,
id: i64,
[min_x, max_x, min_y, max_y]: [f64; 4],
) -> Result<()> {
self.conn.execute(
&format!(
"INSERT INTO {} VALUES (?1, ?2, ?3, ?4, ?5)",
self_scratch_rtree()?
),
rusqlite::params![id, min_x, max_x, min_y, max_y],
)?;
Ok(())
}
}
impl Drop for ScratchDb<'_> {
fn drop(&mut self) {
if let Ok(alias) = quote(SCRATCH_ALIAS) {
best_effort(self.conn, &format!("DETACH DATABASE {alias}"));
}
}
}
fn best_effort(conn: &Connection, sql: &str) {
if conn.execute_batch(sql).is_err() {
}
}
fn self_scratch_rtree() -> Result<String> {
Ok(format!(
"{}.{}",
quote(SCRATCH_ALIAS)?,
quote(SCRATCH_RTREE)?
))
}
pub(crate) fn table_row_count(conn: &Connection, table: &str) -> Result<usize> {
let count: i64 = conn.query_row(
&format!("SELECT count(*) FROM {}", quote(table)?),
[],
|r| r.get(0),
)?;
Ok(usize::try_from(count).unwrap_or(usize::MAX))
}
fn accumulate_envelopes(
conn: &Connection,
table: &str,
geom: &str,
pk: &str,
) -> Result<Vec<(i64, [f64; 4])>> {
let (t, c, i) = (quote(table)?, quote(geom)?, quote(pk)?);
let sql = format!(
"SELECT {i}, ST_MinX({c}), ST_MaxX({c}), ST_MinY({c}), ST_MaxY({c}) \
FROM {t} WHERE {c} NOT NULL AND NOT ST_IsEmpty({c})"
);
let mut stmt = conn.prepare(&sql)?;
let rows = stmt.query_map([], |r| {
Ok((
r.get::<_, i64>(0)?,
[r.get(1)?, r.get(2)?, r.get(3)?, r.get(4)?],
))
})?;
Ok(rows.collect::<rusqlite::Result<_>>()?)
}
fn copy_shadow_tables(conn: &Connection, rtree: &str) -> Result<()> {
for suffix in ["node", "rowid", "parent"] {
let target = quote(&format!("{rtree}_{suffix}"))?;
let source = format!(
"{}.{}",
quote(SCRATCH_ALIAS)?,
quote(&format!("{SCRATCH_RTREE}_{suffix}"))?
);
conn.execute_batch(&format!(
"DELETE FROM {target}; INSERT INTO {target} SELECT * FROM {source};"
))?;
}
Ok(())
}
fn gate(conn: &Connection, rtree: &str, mut expected: HashMap<i64, [f64; 4]>) -> Result<bool> {
let quoted = quote(rtree)?;
let count: i64 = conn.query_row(&format!("SELECT count(*) FROM {quoted}"), [], |r| r.get(0))?;
if usize::try_from(count).unwrap_or(usize::MAX) != expected.len() {
return Ok(false);
}
let mut stmt = conn.prepare(&format!("SELECT id, minx, maxx, miny, maxy FROM {quoted}"))?;
let mut rows = stmt.query([])?;
while let Some(row) = rows.next()? {
let id: i64 = row.get(0)?;
let (s_min_x, s_max_x, s_min_y, s_max_y): (f64, f64, f64, f64) =
(row.get(1)?, row.get(2)?, row.get(3)?, row.get(4)?);
let Some([min_x, max_x, min_y, max_y]) = expected.remove(&id) else {
return Ok(false);
};
if !(s_min_x <= min_x && s_max_x >= max_x && s_min_y <= min_y && s_max_y >= max_y) {
return Ok(false);
}
}
if !expected.is_empty() {
return Ok(false);
}
let integrity: String = conn.query_row("PRAGMA integrity_check", [], |r| r.get(0))?;
Ok(integrity == "ok")
}
pub(crate) fn fill_index<F>(
conn: &Connection,
table: &str,
geom: &str,
pk: &str,
rtree: &str,
tamper: ScratchTamper,
after: F,
) -> Result<BuildPath>
where
F: FnOnce(&Connection) -> Result<()>,
{
let accumulated = accumulate_envelopes(conn, table, geom, pk)?;
let scratch = ScratchDb::attach(conn)?;
scratch.build(&accumulated)?;
tamper(&scratch)?;
let quoted_rtree = quote(rtree)?;
let create_vtab = triggers::create_rtree_table_sql(table, geom)?;
let tx = conn.unchecked_transaction()?;
conn.execute_batch(&format!("DROP TABLE IF EXISTS {quoted_rtree}"))?;
conn.execute_batch(&create_vtab)?;
copy_shadow_tables(conn, rtree)?;
let expected: HashMap<i64, [f64; 4]> = accumulated.into_iter().collect();
let path = if gate(conn, rtree, expected)? {
BuildPath::Bulk
} else {
conn.execute_batch(&format!("DROP TABLE {quoted_rtree}"))?;
conn.execute_batch(&create_vtab)?;
conn.execute_batch(&triggers::populate_rtree_sql(table, geom, pk)?)?;
BuildPath::TriggeredFallback
};
after(conn)?;
tx.commit()?;
Ok(path)
}