use martin_tile_utils::MAX_ZOOM;
use sqlite_compressions::rusqlite::Connection;
use sqlx::{AssertSqlSafe, Executor as _, SqliteConnection, SqliteExecutor, query};
use tracing::debug;
use crate::MbtError::InvalidZoomValue;
use crate::errors::MbtResult;
use crate::{
MbtType, create_cache_tables, create_flat_tables, create_flat_with_hash_tables,
create_normalized_tables, create_tiles_with_hash_view,
};
pub async fn is_empty_database<T>(conn: &mut T) -> MbtResult<bool>
where
for<'e> &'e mut T: SqliteExecutor<'e>,
{
Ok(query!("SELECT 1 as has_rows FROM sqlite_schema LIMIT 1")
.fetch_optional(&mut *conn)
.await?
.is_none())
}
pub(crate) async fn create_schema<T>(conn: &mut T, sql: &str, strict: bool) -> MbtResult<()>
where
for<'e> &'e mut T: SqliteExecutor<'e>,
{
for stmt in sql.split(';') {
let stmt = stmt.trim();
if stmt.is_empty() {
continue;
}
let mut stmt = stmt
.replacen("CREATE TABLE ", "CREATE TABLE IF NOT EXISTS ", 1)
.replacen("CREATE VIEW ", "CREATE VIEW IF NOT EXISTS ", 1);
if strict
&& stmt.to_ascii_uppercase().starts_with("CREATE TABLE")
&& let Some((body, options)) = stmt.rsplit_once(')')
{
let mut stmt2 = format!("{body}) STRICT");
let options = options.trim();
if !options.is_empty() {
stmt2.push_str(", ");
stmt2.push_str(options);
}
stmt = stmt2;
}
conn.execute(AssertSqlSafe(stmt)).await?;
}
Ok(())
}
pub async fn create_metadata_table<T>(conn: &mut T, strict: bool) -> MbtResult<()>
where
for<'e> &'e mut T: SqliteExecutor<'e>,
{
debug!("Creating metadata table if it doesn't already exist");
create_schema(conn, include_str!("../sql/init-metadata.sql"), strict).await
}
pub async fn reset_db_settings<T>(conn: &mut T) -> MbtResult<()>
where
for<'e> &'e mut T: SqliteExecutor<'e>,
{
debug!("Resetting PRAGMA settings and vacuuming");
query!("PRAGMA page_size = 512").execute(&mut *conn).await?;
query!("PRAGMA encoding = 'UTF-8'")
.execute(&mut *conn)
.await?;
query!("VACUUM").execute(&mut *conn).await?;
Ok(())
}
pub async fn init_mbtiles_schema<T>(conn: &mut T, mbt_type: MbtType, strict: bool) -> MbtResult<()>
where
for<'e> &'e mut T: SqliteExecutor<'e>,
{
reset_db_settings(conn).await?;
create_metadata_table(&mut *conn, strict).await?;
match mbt_type {
MbtType::Flat => create_flat_tables(&mut *conn, strict).await,
MbtType::FlatWithHash => create_flat_with_hash_tables(&mut *conn, strict).await,
MbtType::Normalized { hash_view, .. } => {
create_normalized_tables(&mut *conn, strict).await?;
if hash_view {
create_tiles_with_hash_view(&mut *conn).await?;
}
Ok(())
}
MbtType::Cache => create_cache_tables(&mut *conn, strict).await,
}
}
pub async fn detach_db<T>(conn: &mut T, name: &str) -> MbtResult<()>
where
for<'e> &'e mut T: SqliteExecutor<'e>,
{
debug!("Detaching {name}");
query(AssertSqlSafe(format!("DETACH DATABASE {name}")))
.execute(conn)
.await?;
Ok(())
}
fn validate_zoom(zoom: Option<i64>, zoom_name: &'static str) -> MbtResult<Option<u8>> {
if let Some(zoom) = zoom {
let z = u8::try_from(zoom).ok().filter(|v| *v <= MAX_ZOOM);
if z.is_none() {
Err(InvalidZoomValue(zoom_name, zoom.to_string()))
} else {
Ok(z)
}
} else {
Ok(None)
}
}
pub async fn compute_min_max_zoom<T>(conn: &mut T) -> MbtResult<Option<(u8, u8)>>
where
for<'e> &'e mut T: SqliteExecutor<'e>,
{
let info = query!(
"
SELECT min(zoom_level) AS min_zoom,
max(zoom_level) AS max_zoom
FROM tiles;"
)
.fetch_one(conn)
.await?;
let min_zoom = validate_zoom(info.min_zoom, "zoom_level")?;
let max_zoom = validate_zoom(info.max_zoom, "zoom_level")?;
match (min_zoom, max_zoom) {
(Some(min_zoom), Some(max_zoom)) => Ok(Some((min_zoom, max_zoom))),
_ => Ok(None),
}
}
pub async fn action_with_rusqlite(
conn: &mut SqliteConnection,
action: impl FnOnce(&Connection) -> MbtResult<()>,
) -> MbtResult<()> {
let mut handle_lock = conn.lock_handle().await?;
let handle = handle_lock.as_raw_handle().as_ptr();
let rusqlite_conn = unsafe { Connection::from_handle(handle) }?;
action(&rusqlite_conn)
}