use std::ffi::CStr;
use std::path::{Path, PathBuf};
use std::str::Utf8Error;
use std::sync::Arc;
use sqlx::sqlite::{LockedSqliteHandle, SqlitePool};
use thiserror::Error;
use tracing::warn;
use crate::sync::EagerFutureCell;
#[derive(Debug, Error)]
pub enum VersionError {
#[error("failed to parse the sqlite version: {0}")]
Parsing(#[from] semver::Error),
#[error("failed to query the sqlite version: {0}")]
Query(#[from] sqlx::Error),
}
#[derive(Debug, Clone, Error)]
pub enum SqlitePathError {
#[error("the path reported by sqlite is NULL.")]
NullPath,
#[error("the path reported by sqlite is not a utf8 path")]
NonUtf8Path(#[from] Utf8Error),
#[error("failed to acquire a connection to query the sqlite path: {0}")]
Acquire(#[from] Arc<sqlx::Error>),
}
#[derive(Debug, Clone)]
pub struct Info {
ffi_info: FfiInfo,
pub version: Result<semver::Version, Arc<VersionError>>,
}
#[derive(Debug, Clone)]
struct FfiInfo {
variable_number_limit: Option<usize>,
wal_path: Result<PathBuf, SqlitePathError>,
}
impl FfiInfo {
async fn query(pool: &SqlitePool) -> Self {
let mut conn = match pool.acquire().await {
Ok(conn) => conn,
Err(err) => return Self::unavailable(err),
};
let mut handle = match conn.lock_handle().await {
Ok(handle) => handle,
Err(err) => return Self::unavailable(err),
};
let vnl = Self::query_variable_number_limit(&mut handle);
let wal_path = Self::query_wal_path(&mut handle);
drop(handle);
Self {
variable_number_limit: vnl,
wal_path,
}
}
fn unavailable(err: sqlx::Error) -> Self {
Self {
variable_number_limit: None,
wal_path: Err(SqlitePathError::Acquire(Arc::new(err))),
}
}
fn query_variable_number_limit(handle: &mut LockedSqliteHandle<'_>) -> Option<usize> {
let raw_handle = handle.as_raw_handle();
#[allow(unsafe_code, reason = "FFI call to read SQLITE_LIMIT_VARIABLE_NUMBER")]
let limit = unsafe {
libsqlite3_sys::sqlite3_limit(
raw_handle.as_ptr(),
libsqlite3_sys::SQLITE_LIMIT_VARIABLE_NUMBER,
-1,
)
};
match usize::try_from(limit) {
Ok(l) => Some(l),
Err(err) => {
warn!(
"failed to convert {limit} to a number to compute bind param count: {err}. \
performance could be degraded."
);
None
}
}
}
fn query_wal_path(handle: &mut LockedSqliteHandle<'_>) -> Result<PathBuf, SqlitePathError> {
#[allow(unsafe_code)]
let db_filename = unsafe {
libsqlite3_sys::sqlite3_db_filename(handle.as_raw_handle().as_ptr(), c"main".as_ptr())
};
if db_filename.is_null() {
return Err(SqlitePathError::NullPath);
}
#[allow(unsafe_code)]
let wal_filename = unsafe { libsqlite3_sys::sqlite3_filename_wal(db_filename) };
if wal_filename.is_null() {
return Err(SqlitePathError::NullPath);
}
#[allow(unsafe_code)]
let file_cstr = unsafe { CStr::from_ptr(wal_filename).to_bytes() };
Ok(PathBuf::from(std::str::from_utf8(file_cstr)?))
}
}
impl Info {
const MAX_BIND_PARAMS_FALLBACK: usize = 999;
pub fn new_eager_future(pool: SqlitePool) -> EagerFutureCell<Self> {
EagerFutureCell::new(
async move {
let version = Self::query_version(&pool).await.map_err(Arc::new);
let ffi_info = FfiInfo::query(&pool).await;
Self { ffi_info, version }
},
&tokio::runtime::Handle::current(),
)
}
async fn query_version(pool: &SqlitePool) -> Result<semver::Version, VersionError> {
let str: String = sqlx::query_scalar("SELECT sqlite_version()").fetch_one(pool).await?;
Ok(semver::Version::parse(&str)?)
}
pub fn variable_number_limit(&self) -> usize {
self.ffi_info.variable_number_limit.unwrap_or(Self::MAX_BIND_PARAMS_FALLBACK)
}
pub fn wal_path(&self) -> Result<&Path, SqlitePathError> {
self.ffi_info.wal_path.as_deref().map_err(Clone::clone)
}
}