use std::{
ffi::CString,
io,
ptr::{null, null_mut},
sync::atomic::{AtomicU64, Ordering},
time::Duration,
};
use libsqlite3_sys::{
SQLITE_OPEN_CREATE, SQLITE_OPEN_FULLMUTEX, SQLITE_OPEN_MEMORY, SQLITE_OPEN_NOMUTEX,
SQLITE_OPEN_PRIVATECACHE, SQLITE_OPEN_READONLY, SQLITE_OPEN_READWRITE, SQLITE_OPEN_SHAREDCACHE,
};
use crate::{
Error, Musq, Result,
sqlite::{
connection::{ConnectionState, LogSettings, StatementCache, handle::ConnectionHandle},
ffi,
},
};
static THREAD_ID: AtomicU64 = AtomicU64::new(0);
pub struct EstablishParams {
filename: CString,
open_flags: i32,
busy_timeout: Duration,
log_settings: LogSettings,
statement_cache_capacity: usize,
floating_point_text_digits: Option<u8>,
parser_depth_limit: Option<i32>,
pub(crate) thread_name: String,
pub(crate) command_channel_size: usize,
}
impl EstablishParams {
pub fn from_options(options: &Musq) -> Result<Self> {
let mut filename = options
.filename
.to_str()
.ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
"filename passed to SQLite must be valid UTF-8",
)
})?
.to_owned();
let mut flags = if options.serialized {
SQLITE_OPEN_FULLMUTEX
} else {
SQLITE_OPEN_NOMUTEX
};
flags |= if options.read_only {
SQLITE_OPEN_READONLY
} else if options.create_if_missing {
SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE
} else {
SQLITE_OPEN_READWRITE
};
if options.in_memory {
flags |= SQLITE_OPEN_MEMORY;
}
flags |= if options.shared_cache {
SQLITE_OPEN_SHAREDCACHE
} else {
SQLITE_OPEN_PRIVATECACHE
};
let mut query_params: Vec<String> = vec![];
if options.immutable {
query_params.push("immutable=true".into())
}
if let Some(vfs) = &options.vfs {
query_params.push(format!("vfs={vfs}"))
}
if !query_params.is_empty() {
filename = format!("file:{}?{}", filename, query_params.join("&"));
flags |= libsqlite3_sys::SQLITE_OPEN_URI;
}
let filename = CString::new(filename).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidData,
"filename passed to SQLite must not contain nul bytes",
)
})?;
let floating_point_text_digits =
validate_floating_point_text_digits(options.floating_point_text_digits)?;
let parser_depth_limit = validate_parser_depth_limit(options.parser_depth_limit)?;
Ok(Self {
filename,
open_flags: flags,
busy_timeout: options.busy_timeout,
log_settings: options.log_settings.clone(),
statement_cache_capacity: options.statement_cache_capacity,
floating_point_text_digits,
parser_depth_limit,
thread_name: (options.thread_name)(THREAD_ID.fetch_add(1, Ordering::AcqRel)),
command_channel_size: options.command_channel_size,
})
}
pub(crate) fn establish(&self) -> Result<ConnectionState> {
#[cfg(feature = "vec")]
ffi::register_vec()?;
let mut handle = null_mut();
let open_res = ffi::open_v2(self.filename.as_ptr(), &mut handle, self.open_flags, null());
if let Err(e) = open_res {
return Err(e.into());
}
if handle.is_null() {
return Err(Error::Io(io::Error::new(
io::ErrorKind::OutOfMemory,
"SQLite is unable to allocate memory to hold the sqlite3 object",
)));
}
let handle = unsafe { ConnectionHandle::new(handle) };
ffi::extended_result_codes(handle.as_ptr(), 1).map_err(Error::from)?;
let ms = i32::try_from(self.busy_timeout.as_millis()).unwrap_or(i32::MAX);
ffi::busy_timeout(handle.as_ptr(), ms).map_err(Error::from)?;
if let Some(digits) = self.floating_point_text_digits {
let configured = ffi::db_config_fp_digits(handle.as_ptr(), i32::from(digits))
.map_err(Error::from)?;
if configured != i32::from(digits) {
return Err(Error::Protocol(format!(
"SQLite reported floating point text digits {configured} after setting {digits}"
)));
}
}
if let Some(limit) = self.parser_depth_limit {
ffi::limit(
handle.as_ptr(),
libsqlite3_sys::SQLITE_LIMIT_PARSER_DEPTH,
limit,
);
}
Ok(ConnectionState {
handle,
statements: StatementCache::new(self.statement_cache_capacity),
transaction_depth: 0,
log_settings: self.log_settings.clone(),
})
}
}
fn validate_floating_point_text_digits(digits: Option<u8>) -> Result<Option<u8>> {
if let Some(digits) = digits
&& !(4..=23).contains(&digits)
{
return Err(Error::Protocol(format!(
"floating_point_text_digits must be between 4 and 23, got {digits}"
)));
}
Ok(digits)
}
fn validate_parser_depth_limit(limit: Option<u32>) -> Result<Option<i32>> {
let Some(limit) = limit else {
return Ok(None);
};
if limit == 0 {
return Err(Error::Protocol(
"parser_depth_limit must be greater than zero".into(),
));
}
i32::try_from(limit)
.map(Some)
.map_err(|_| Error::Protocol(format!("parser_depth_limit must fit into i32, got {limit}")))
}