logiguard fork v3: full patch set on verified 8c74db0 tree

Includes prior-session patches (carry forward so the app compiles):
  - crates/gpui/build.rs: cross-compile manifest fix
  - crates/gpui/src/platform.rs: PlatformWindow::activate_with_token trait method
  - crates/gpui/src/window.rs: Window::activate_with_token public API
  - crates/gpui_linux/src/linux/wayland/window.rs: WaylandWindow::activate_with_token + activate() keyboard-serial fix

Plus the focus-serial fix:
  - serial.rs: SerialKind::KeyboardEnter
  - client.rs: store wl_keyboard.enter serial; latest_serial_of()

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mohamad Khani
2026-07-14 01:52:12 +03:30
commit b9819977a5
3984 changed files with 1487015 additions and 0 deletions

2
crates/sqlez/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
debug/
target/

23
crates/sqlez/Cargo.toml Normal file
View File

@@ -0,0 +1,23 @@
[package]
name = "sqlez"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[dependencies]
anyhow.workspace = true
collections.workspace = true
futures.workspace = true
indoc.workspace = true
libsqlite3-sys.workspace = true
log.workspace = true
parking_lot.workspace = true
pollster.workspace = true
sqlformat.workspace = true
thread_local = "1.1.4"
util.workspace = true
uuid.workspace = true

1
crates/sqlez/LICENSE-GPL Symbolic link
View File

@@ -0,0 +1 @@
../../LICENSE-GPL

View File

@@ -0,0 +1,466 @@
use std::{
path::{Path, PathBuf},
sync::Arc,
};
use anyhow::{Context as _, Result};
use util::paths::PathExt;
use crate::statement::{SqlType, Statement};
/// Define the number of columns that a type occupies in a query/database
pub trait StaticColumnCount {
fn column_count() -> usize {
1
}
}
/// Bind values of different types to placeholders in a prepared SQL statement.
pub trait Bind {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32>;
}
pub trait Column: Sized {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)>;
}
impl StaticColumnCount for bool {}
impl Bind for bool {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
statement
.bind(&self.then_some(1).unwrap_or(0), start_index)
.with_context(|| format!("Failed to bind bool at index {start_index}"))
}
}
impl Column for bool {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
i32::column(statement, start_index)
.map(|(i, next_index)| (i != 0, next_index))
.with_context(|| format!("Failed to read bool at index {start_index}"))
}
}
impl StaticColumnCount for &[u8] {}
impl Bind for &[u8] {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
statement
.bind_blob(start_index, self)
.with_context(|| format!("Failed to bind &[u8] at index {start_index}"))?;
Ok(start_index + 1)
}
}
impl<const C: usize> StaticColumnCount for &[u8; C] {}
impl<const C: usize> Bind for &[u8; C] {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
statement
.bind_blob(start_index, self.as_slice())
.with_context(|| format!("Failed to bind &[u8; C] at index {start_index}"))?;
Ok(start_index + 1)
}
}
impl<const C: usize> Column for [u8; C] {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let bytes_slice = statement.column_blob(start_index)?;
let array = bytes_slice.try_into()?;
Ok((array, start_index + 1))
}
}
impl StaticColumnCount for Vec<u8> {}
impl Bind for Vec<u8> {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
statement
.bind_blob(start_index, self)
.with_context(|| format!("Failed to bind Vec<u8> at index {start_index}"))?;
Ok(start_index + 1)
}
}
impl Column for Vec<u8> {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let result = statement
.column_blob(start_index)
.with_context(|| format!("Failed to read Vec<u8> at index {start_index}"))?;
Ok((Vec::from(result), start_index + 1))
}
}
impl StaticColumnCount for f64 {}
impl Bind for f64 {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
statement
.bind_double(start_index, *self)
.with_context(|| format!("Failed to bind f64 at index {start_index}"))?;
Ok(start_index + 1)
}
}
impl Column for f64 {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let result = statement
.column_double(start_index)
.with_context(|| format!("Failed to parse f64 at index {start_index}"))?;
Ok((result, start_index + 1))
}
}
impl StaticColumnCount for f32 {}
impl Bind for f32 {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
statement
.bind_double(start_index, *self as f64)
.with_context(|| format!("Failed to bind f64 at index {start_index}"))?;
Ok(start_index + 1)
}
}
impl Column for f32 {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let result = statement
.column_double(start_index)
.with_context(|| format!("Failed to parse f32 at index {start_index}"))?
as f32;
Ok((result, start_index + 1))
}
}
impl StaticColumnCount for i32 {}
impl Bind for i32 {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
statement
.bind_int(start_index, *self)
.with_context(|| format!("Failed to bind i32 at index {start_index}"))?;
Ok(start_index + 1)
}
}
impl Column for i32 {
fn column<'a>(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let result = statement.column_int(start_index)?;
Ok((result, start_index + 1))
}
}
impl StaticColumnCount for i64 {}
impl Bind for i64 {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
statement
.bind_int64(start_index, *self)
.with_context(|| format!("Failed to bind i64 at index {start_index}"))?;
Ok(start_index + 1)
}
}
impl Column for i64 {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let result = statement.column_int64(start_index)?;
Ok((result, start_index + 1))
}
}
impl StaticColumnCount for u64 {}
impl Bind for u64 {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
statement
.bind_int64(start_index, (*self) as i64)
.with_context(|| format!("Failed to bind i64 at index {start_index}"))?;
Ok(start_index + 1)
}
}
impl Column for u64 {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let result = statement.column_int64(start_index)? as u64;
Ok((result, start_index + 1))
}
}
impl StaticColumnCount for u32 {}
impl Bind for u32 {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
(*self as i64)
.bind(statement, start_index)
.with_context(|| format!("Failed to bind usize at index {start_index}"))
}
}
impl Column for u32 {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let result = statement.column_int64(start_index)?;
Ok((result as u32, start_index + 1))
}
}
impl StaticColumnCount for u16 {}
impl Bind for u16 {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
(*self as i64)
.bind(statement, start_index)
.with_context(|| format!("Failed to bind usize at index {start_index}"))
}
}
impl Column for u16 {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let result = statement.column_int64(start_index)?;
Ok((result as u16, start_index + 1))
}
}
impl StaticColumnCount for usize {}
impl Bind for usize {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
(*self as i64)
.bind(statement, start_index)
.with_context(|| format!("Failed to bind usize at index {start_index}"))
}
}
impl Column for usize {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let result = statement.column_int64(start_index)?;
Ok((result as usize, start_index + 1))
}
}
impl StaticColumnCount for &str {}
impl Bind for &str {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
statement.bind_text(start_index, self)?;
Ok(start_index + 1)
}
}
impl StaticColumnCount for Arc<str> {}
impl Bind for Arc<str> {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
statement.bind_text(start_index, self.as_ref())?;
Ok(start_index + 1)
}
}
impl StaticColumnCount for String {}
impl Bind for String {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
statement.bind_text(start_index, self)?;
Ok(start_index + 1)
}
}
impl Column for Arc<str> {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let result = statement.column_text(start_index)?;
Ok((Arc::from(result), start_index + 1))
}
}
impl Column for String {
fn column<'a>(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let result = statement.column_text(start_index)?;
Ok((result.to_owned(), start_index + 1))
}
}
impl<T: StaticColumnCount> StaticColumnCount for Option<T> {
fn column_count() -> usize {
T::column_count()
}
}
impl<T: Bind + StaticColumnCount> Bind for Option<T> {
fn bind(&self, statement: &Statement, mut start_index: i32) -> Result<i32> {
if let Some(this) = self {
this.bind(statement, start_index)
} else {
for _ in 0..T::column_count() {
statement.bind_null(start_index)?;
start_index += 1;
}
Ok(start_index)
}
}
}
impl<T: Column + StaticColumnCount> Column for Option<T> {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
if let SqlType::Null = statement.column_type(start_index)? {
Ok((None, start_index + T::column_count() as i32))
} else {
T::column(statement, start_index).map(|(result, next_index)| (Some(result), next_index))
}
}
}
impl<T: StaticColumnCount, const COUNT: usize> StaticColumnCount for [T; COUNT] {
fn column_count() -> usize {
T::column_count() * COUNT
}
}
impl<T: Bind, const COUNT: usize> Bind for [T; COUNT] {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
let mut current_index = start_index;
for binding in self {
current_index = binding.bind(statement, current_index)?
}
Ok(current_index)
}
}
impl StaticColumnCount for &Path {}
impl Bind for &Path {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
self.as_os_str()
.as_encoded_bytes()
.bind(statement, start_index)
}
}
impl StaticColumnCount for Arc<Path> {}
impl Bind for Arc<Path> {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
self.as_ref().bind(statement, start_index)
}
}
impl Column for Arc<Path> {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let blob = statement.column_blob(start_index)?;
PathBuf::try_from_bytes(blob).map(|path| (Arc::from(path.as_path()), start_index + 1))
}
}
impl StaticColumnCount for PathBuf {}
impl Bind for PathBuf {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
(self.as_ref() as &Path).bind(statement, start_index)
}
}
impl Column for PathBuf {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let blob = statement.column_blob(start_index)?;
PathBuf::try_from_bytes(blob).map(|path| (path, start_index + 1))
}
}
impl StaticColumnCount for uuid::Uuid {
fn column_count() -> usize {
1
}
}
impl Bind for uuid::Uuid {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
self.as_bytes().bind(statement, start_index)
}
}
impl Column for uuid::Uuid {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let (bytes, next_index) = Column::column(statement, start_index)?;
Ok((uuid::Uuid::from_bytes(bytes), next_index))
}
}
impl StaticColumnCount for () {
fn column_count() -> usize {
0
}
}
/// Unit impls do nothing. This simplifies query macros
impl Bind for () {
fn bind(&self, _statement: &Statement, start_index: i32) -> Result<i32> {
Ok(start_index)
}
}
impl Column for () {
fn column(_statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
Ok(((), start_index))
}
}
macro_rules! impl_tuple_row_traits {
( $($local:ident: $type:ident),+ ) => {
impl<$($type: StaticColumnCount),+> StaticColumnCount for ($($type,)+) {
fn column_count() -> usize {
let mut count = 0;
$(count += $type::column_count();)+
count
}
}
impl<$($type: Bind),+> Bind for ($($type,)+) {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
let mut next_index = start_index;
let ($($local,)+) = self;
$(next_index = $local.bind(statement, next_index)?;)+
Ok(next_index)
}
}
impl<$($type: Column),+> Column for ($($type,)+) {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let mut next_index = start_index;
Ok((
(
$({
let value;
(value, next_index) = $type::column(statement, next_index)?;
value
},)+
),
next_index,
))
}
}
}
}
impl_tuple_row_traits!(t1: T1, t2: T2);
impl_tuple_row_traits!(t1: T1, t2: T2, t3: T3);
impl_tuple_row_traits!(t1: T1, t2: T2, t3: T3, t4: T4);
impl_tuple_row_traits!(t1: T1, t2: T2, t3: T3, t4: T4, t5: T5);
impl_tuple_row_traits!(t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6);
impl_tuple_row_traits!(t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7);
impl_tuple_row_traits!(
t1: T1,
t2: T2,
t3: T3,
t4: T4,
t5: T5,
t6: T6,
t7: T7,
t8: T8
);
impl_tuple_row_traits!(
t1: T1,
t2: T2,
t3: T3,
t4: T4,
t5: T5,
t6: T6,
t7: T7,
t8: T8,
t9: T9
);
impl_tuple_row_traits!(
t1: T1,
t2: T2,
t3: T3,
t4: T4,
t5: T5,
t6: T6,
t7: T7,
t8: T8,
t9: T9,
t10: T10
);

View File

@@ -0,0 +1,532 @@
use std::{
cell::RefCell,
ffi::{CStr, CString},
marker::PhantomData,
path::Path,
ptr,
};
use anyhow::Result;
use libsqlite3_sys::*;
pub struct Connection {
pub(crate) sqlite3: *mut sqlite3,
persistent: bool,
pub(crate) write: RefCell<bool>,
_sqlite: PhantomData<sqlite3>,
}
unsafe impl Send for Connection {}
impl Connection {
fn open_with_flags(uri: &str, persistent: bool, flags: i32) -> Result<Self> {
let mut connection = Self {
sqlite3: ptr::null_mut(),
persistent,
write: RefCell::new(true),
_sqlite: PhantomData,
};
unsafe {
sqlite3_open_v2(
CString::new(uri)?.as_ptr(),
&mut connection.sqlite3,
flags,
ptr::null(),
);
// Turn on extended error codes
sqlite3_extended_result_codes(connection.sqlite3, 1);
connection.last_error()?;
}
Ok(connection)
}
pub(crate) fn open(uri: &str, persistent: bool) -> Result<Self> {
Self::open_with_flags(
uri,
persistent,
SQLITE_OPEN_CREATE | SQLITE_OPEN_NOMUTEX | SQLITE_OPEN_READWRITE,
)
}
/// Attempts to open the database at uri. If it fails, a shared memory db will be opened
/// instead.
pub fn open_file(uri: &str) -> Self {
Self::open(uri, true).unwrap_or_else(|_| Self::open_memory(Some(uri)))
}
pub fn open_memory(uri: Option<&str>) -> Self {
if let Some(uri) = uri {
let in_memory_path = format!("file:{}?mode=memory&cache=shared", uri);
return Self::open_with_flags(
&in_memory_path,
false,
SQLITE_OPEN_CREATE | SQLITE_OPEN_NOMUTEX | SQLITE_OPEN_READWRITE | SQLITE_OPEN_URI,
)
.expect("Could not create fallback in memory db");
} else {
Self::open(":memory:", false).expect("Could not create fallback in memory db")
}
}
pub fn persistent(&self) -> bool {
self.persistent
}
pub fn can_write(&self) -> bool {
*self.write.borrow()
}
pub fn backup_main(&self, destination: &Connection) -> Result<()> {
unsafe {
let backup = sqlite3_backup_init(
destination.sqlite3,
CString::new("main")?.as_ptr(),
self.sqlite3,
CString::new("main")?.as_ptr(),
);
sqlite3_backup_step(backup, -1);
sqlite3_backup_finish(backup);
destination.last_error()
}
}
pub fn backup_main_to(&self, destination: impl AsRef<Path>) -> Result<()> {
let destination = Self::open_file(destination.as_ref().to_string_lossy().as_ref());
self.backup_main(&destination)
}
pub fn sql_has_syntax_error(&self, sql: &str) -> Option<(String, usize)> {
let sql = CString::new(sql).unwrap();
let mut remaining_sql = sql.as_c_str();
let sql_start = remaining_sql.as_ptr();
let mut alter_table = None;
while {
let remaining_sql_str = remaining_sql.to_str().unwrap().trim();
let any_remaining_sql = remaining_sql_str != ";" && !remaining_sql_str.is_empty();
if any_remaining_sql {
alter_table = parse_alter_table(remaining_sql_str);
}
any_remaining_sql
} {
let mut raw_statement = ptr::null_mut::<sqlite3_stmt>();
let mut remaining_sql_ptr = ptr::null();
let (res, offset, message, _conn) = if let Some((table_to_alter, column)) = alter_table
{
// ALTER TABLE is a weird statement. When preparing the statement the table's
// existence is checked *before* syntax checking any other part of the statement.
// Therefore, we need to make sure that the table has been created before calling
// prepare. As we don't want to trash whatever database this is connected to, we
// create a new in-memory DB to test.
let temp_connection = Connection::open_memory(None);
//This should always succeed, if it doesn't then you really should know about it
temp_connection
.exec(&format!("CREATE TABLE {table_to_alter}({column})"))
.unwrap()()
.unwrap();
unsafe {
sqlite3_prepare_v2(
temp_connection.sqlite3,
remaining_sql.as_ptr(),
-1,
&mut raw_statement,
&mut remaining_sql_ptr,
)
};
#[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
let offset = unsafe { sqlite3_error_offset(temp_connection.sqlite3) };
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
let offset = 0;
unsafe {
(
sqlite3_errcode(temp_connection.sqlite3),
offset,
sqlite3_errmsg(temp_connection.sqlite3),
Some(temp_connection),
)
}
} else {
unsafe {
sqlite3_prepare_v2(
self.sqlite3,
remaining_sql.as_ptr(),
-1,
&mut raw_statement,
&mut remaining_sql_ptr,
)
};
#[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
let offset = unsafe { sqlite3_error_offset(self.sqlite3) };
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
let offset = 0;
unsafe {
(
sqlite3_errcode(self.sqlite3),
offset,
sqlite3_errmsg(self.sqlite3),
None,
)
}
};
unsafe { sqlite3_finalize(raw_statement) };
if res == 1 && offset >= 0 {
let sub_statement_correction = remaining_sql.as_ptr() as usize - sql_start as usize;
let err_msg = String::from_utf8_lossy(unsafe {
CStr::from_ptr(message as *const _).to_bytes()
})
.into_owned();
return Some((err_msg, offset as usize + sub_statement_correction));
}
remaining_sql = unsafe { CStr::from_ptr(remaining_sql_ptr) };
alter_table = None;
}
None
}
pub(crate) fn last_error(&self) -> Result<()> {
unsafe {
let code = sqlite3_errcode(self.sqlite3);
const NON_ERROR_CODES: &[i32] = &[SQLITE_OK, SQLITE_ROW];
if NON_ERROR_CODES.contains(&code) {
return Ok(());
}
let message = sqlite3_errmsg(self.sqlite3);
let message = if message.is_null() {
None
} else {
Some(
String::from_utf8_lossy(CStr::from_ptr(message as *const _).to_bytes())
.into_owned(),
)
};
anyhow::bail!("Sqlite call failed with code {code} and message: {message:?}")
}
}
pub(crate) fn with_write<T>(&self, callback: impl FnOnce(&Connection) -> T) -> T {
*self.write.borrow_mut() = true;
let result = callback(self);
*self.write.borrow_mut() = false;
result
}
}
fn parse_alter_table(remaining_sql_str: &str) -> Option<(String, String)> {
let remaining_sql_str = remaining_sql_str.to_lowercase();
if remaining_sql_str.starts_with("alter")
&& let Some(table_offset) = remaining_sql_str.find("table")
{
let after_table_offset = table_offset + "table".len();
let table_to_alter = remaining_sql_str
.chars()
.skip(after_table_offset)
.skip_while(|c| c.is_whitespace())
.take_while(|c| !c.is_whitespace())
.collect::<String>();
if !table_to_alter.is_empty() {
let column_name = if let Some(rename_offset) = remaining_sql_str.find("rename column") {
let after_rename_offset = rename_offset + "rename column".len();
remaining_sql_str
.chars()
.skip(after_rename_offset)
.skip_while(|c| c.is_whitespace())
.take_while(|c| !c.is_whitespace())
.collect::<String>()
} else if let Some(drop_offset) = remaining_sql_str.find("drop column") {
let after_drop_offset = drop_offset + "drop column".len();
remaining_sql_str
.chars()
.skip(after_drop_offset)
.skip_while(|c| c.is_whitespace())
.take_while(|c| !c.is_whitespace())
.collect::<String>()
} else {
"__place_holder_column_for_syntax_checking".to_string()
};
return Some((table_to_alter, column_name));
}
}
None
}
impl Drop for Connection {
fn drop(&mut self) {
unsafe { sqlite3_close(self.sqlite3) };
}
}
#[cfg(test)]
mod test {
use anyhow::Result;
use indoc::indoc;
use std::{
fs,
sync::atomic::{AtomicUsize, Ordering},
};
use crate::connection::Connection;
static NEXT_NAMED_MEMORY_DB_ID: AtomicUsize = AtomicUsize::new(0);
fn unique_named_memory_db(prefix: &str) -> String {
format!(
"{prefix}_{}_{}",
std::process::id(),
NEXT_NAMED_MEMORY_DB_ID.fetch_add(1, Ordering::Relaxed)
)
}
fn literal_named_memory_paths(name: &str) -> [String; 3] {
let main = format!("file:{name}?mode=memory&cache=shared");
[main.clone(), format!("{main}-wal"), format!("{main}-shm")]
}
struct NamedMemoryPathGuard {
paths: [String; 3],
}
impl NamedMemoryPathGuard {
fn new(name: &str) -> Self {
let paths = literal_named_memory_paths(name);
for path in &paths {
let _ = fs::remove_file(path);
}
Self { paths }
}
}
impl Drop for NamedMemoryPathGuard {
fn drop(&mut self) {
for path in &self.paths {
let _ = fs::remove_file(path);
}
}
}
#[test]
fn string_round_trips() -> Result<()> {
let connection = Connection::open_memory(Some("string_round_trips"));
connection
.exec(indoc! {"
CREATE TABLE text (
text TEXT
);"})
.unwrap()()
.unwrap();
let text = "Some test text";
connection
.exec_bound("INSERT INTO text (text) VALUES (?);")
.unwrap()(text)
.unwrap();
assert_eq!(
connection.select_row("SELECT text FROM text;").unwrap()().unwrap(),
Some(text.to_string())
);
Ok(())
}
#[test]
fn tuple_round_trips() {
let connection = Connection::open_memory(Some("tuple_round_trips"));
connection
.exec(indoc! {"
CREATE TABLE test (
text TEXT,
integer INTEGER,
blob BLOB
);"})
.unwrap()()
.unwrap();
let tuple1 = ("test".to_string(), 64, vec![0, 1, 2, 4, 8, 16, 32, 64]);
let tuple2 = ("test2".to_string(), 32, vec![64, 32, 16, 8, 4, 2, 1, 0]);
let mut insert = connection
.exec_bound::<(String, usize, Vec<u8>)>(
"INSERT INTO test (text, integer, blob) VALUES (?, ?, ?)",
)
.unwrap();
insert(tuple1.clone()).unwrap();
insert(tuple2.clone()).unwrap();
assert_eq!(
connection
.select::<(String, usize, Vec<u8>)>("SELECT * FROM test")
.unwrap()()
.unwrap(),
vec![tuple1, tuple2]
);
}
#[test]
fn bool_round_trips() {
let connection = Connection::open_memory(Some("bool_round_trips"));
connection
.exec(indoc! {"
CREATE TABLE bools (
t INTEGER,
f INTEGER
);"})
.unwrap()()
.unwrap();
connection
.exec_bound("INSERT INTO bools(t, f) VALUES (?, ?)")
.unwrap()((true, false))
.unwrap();
assert_eq!(
connection
.select_row::<(bool, bool)>("SELECT * FROM bools;")
.unwrap()()
.unwrap(),
Some((true, false))
);
}
#[test]
fn backup_works() {
let connection1 = Connection::open_memory(Some("backup_works"));
connection1
.exec(indoc! {"
CREATE TABLE blobs (
data BLOB
);"})
.unwrap()()
.unwrap();
let blob = vec![0, 1, 2, 4, 8, 16, 32, 64];
connection1
.exec_bound::<Vec<u8>>("INSERT INTO blobs (data) VALUES (?);")
.unwrap()(blob.clone())
.unwrap();
// Backup connection1 to connection2
let connection2 = Connection::open_memory(Some("backup_works_other"));
connection1.backup_main(&connection2).unwrap();
// Delete the added blob and verify its deleted on the other side
let read_blobs = connection1
.select::<Vec<u8>>("SELECT * FROM blobs;")
.unwrap()()
.unwrap();
assert_eq!(read_blobs, vec![blob]);
}
#[test]
fn named_memory_connections_do_not_create_literal_backing_files() {
let name = unique_named_memory_db("named_memory_connections_do_not_create_backing_files");
let guard = NamedMemoryPathGuard::new(&name);
let connection1 = Connection::open_memory(Some(&name));
connection1
.exec(indoc! {"
CREATE TABLE shared (
value INTEGER
)"})
.unwrap()()
.unwrap();
connection1
.exec("INSERT INTO shared (value) VALUES (7)")
.unwrap()()
.unwrap();
let connection2 = Connection::open_memory(Some(&name));
assert_eq!(
connection2
.select_row::<i64>("SELECT value FROM shared")
.unwrap()()
.unwrap(),
Some(7)
);
for path in &guard.paths {
assert!(
fs::metadata(path).is_err(),
"named in-memory database unexpectedly created backing file {path}"
);
}
}
#[test]
fn multi_step_statement_works() {
let connection = Connection::open_memory(Some("multi_step_statement_works"));
connection
.exec(indoc! {"
CREATE TABLE test (
col INTEGER
)"})
.unwrap()()
.unwrap();
connection
.exec(indoc! {"
INSERT INTO test(col) VALUES (2)"})
.unwrap()()
.unwrap();
assert_eq!(
connection
.select_row::<usize>("SELECT * FROM test")
.unwrap()()
.unwrap(),
Some(2)
);
}
#[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
#[test]
fn test_sql_has_syntax_errors() {
let connection = Connection::open_memory(Some("test_sql_has_syntax_errors"));
let first_stmt =
"CREATE TABLE kv_store(key TEXT PRIMARY KEY, value TEXT NOT NULL) STRICT ;";
let second_stmt = "SELECT FROM";
let second_offset = connection.sql_has_syntax_error(second_stmt).unwrap().1;
let res = connection
.sql_has_syntax_error(&format!("{}\n{}", first_stmt, second_stmt))
.map(|(_, offset)| offset);
assert_eq!(res, Some(first_stmt.len() + second_offset + 1));
}
#[test]
fn test_alter_table_syntax() {
let connection = Connection::open_memory(Some("test_alter_table_syntax"));
assert!(
connection
.sql_has_syntax_error("ALTER TABLE test ADD x TEXT")
.is_none()
);
assert!(
connection
.sql_has_syntax_error("ALTER TABLE test AAD x TEXT")
.is_some()
);
}
}

View File

@@ -0,0 +1,64 @@
use crate::connection::Connection;
pub trait Domain: 'static {
const NAME: &str;
const MIGRATIONS: &[&str];
fn should_allow_migration_change(_index: usize, _old: &str, _new: &str) -> bool {
false
}
}
pub trait Migrator: 'static {
fn migrate(connection: &Connection) -> anyhow::Result<()>;
}
impl Migrator for () {
fn migrate(_connection: &Connection) -> anyhow::Result<()> {
Ok(()) // Do nothing
}
}
impl<D: Domain> Migrator for D {
fn migrate(connection: &Connection) -> anyhow::Result<()> {
connection.migrate(
Self::NAME,
Self::MIGRATIONS,
&mut Self::should_allow_migration_change,
)
}
}
impl<D1: Domain, D2: Domain> Migrator for (D1, D2) {
fn migrate(connection: &Connection) -> anyhow::Result<()> {
D1::migrate(connection)?;
D2::migrate(connection)
}
}
impl<D1: Domain, D2: Domain, D3: Domain> Migrator for (D1, D2, D3) {
fn migrate(connection: &Connection) -> anyhow::Result<()> {
D1::migrate(connection)?;
D2::migrate(connection)?;
D3::migrate(connection)
}
}
impl<D1: Domain, D2: Domain, D3: Domain, D4: Domain> Migrator for (D1, D2, D3, D4) {
fn migrate(connection: &Connection) -> anyhow::Result<()> {
D1::migrate(connection)?;
D2::migrate(connection)?;
D3::migrate(connection)?;
D4::migrate(connection)
}
}
impl<D1: Domain, D2: Domain, D3: Domain, D4: Domain, D5: Domain> Migrator for (D1, D2, D3, D4, D5) {
fn migrate(connection: &Connection) -> anyhow::Result<()> {
D1::migrate(connection)?;
D2::migrate(connection)?;
D3::migrate(connection)?;
D4::migrate(connection)?;
D5::migrate(connection)
}
}

11
crates/sqlez/src/lib.rs Normal file
View File

@@ -0,0 +1,11 @@
pub mod bindable;
pub mod connection;
pub mod domain;
pub mod migrations;
pub mod savepoint;
pub mod statement;
pub mod thread_safe_connection;
pub mod typed_statements;
mod util;
pub use anyhow;

View File

@@ -0,0 +1,390 @@
// Migrations are constructed by domain, and stored in a table in the connection db with domain name,
// effected tables, actual query text, and order.
// If a migration is run and any of the query texts don't match, the app panics on startup (maybe fallback
// to creating a new db?)
// Otherwise any missing migrations are run on the connection
use std::ffi::CString;
use anyhow::{Context as _, Result};
use indoc::{formatdoc, indoc};
use libsqlite3_sys::sqlite3_exec;
use crate::connection::Connection;
impl Connection {
fn eager_exec(&self, sql: &str) -> anyhow::Result<()> {
let sql_str = CString::new(sql).context("Error creating cstr")?;
unsafe {
sqlite3_exec(
self.sqlite3,
sql_str.as_c_str().as_ptr(),
None,
std::ptr::null_mut(),
std::ptr::null_mut(),
);
}
self.last_error()
.with_context(|| format!("Prepare call failed for query:\n{}", sql))?;
Ok(())
}
/// Migrate the database, for the given domain.
/// Note: Unlike everything else in SQLez, migrations are run eagerly, without first
/// preparing the SQL statements. This makes it possible to do multi-statement schema
/// updates in a single string without running into prepare errors.
pub fn migrate(
&self,
domain: &'static str,
migrations: &[&'static str],
should_allow_migration_change: &mut dyn FnMut(usize, &str, &str) -> bool,
) -> Result<()> {
self.with_savepoint("migrating", || {
// Setup the migrations table unconditionally
self.exec(indoc! {"
CREATE TABLE IF NOT EXISTS migrations (
domain TEXT,
step INTEGER,
migration TEXT
)"})?()?;
let completed_migrations =
self.select_bound::<&str, (String, usize, String)>(indoc! {"
SELECT domain, step, migration FROM migrations
WHERE domain = ?
ORDER BY step
"})?(domain)?;
let mut store_completed_migration = self
.exec_bound("INSERT INTO migrations (domain, step, migration) VALUES (?, ?, ?)")?;
let mut did_migrate = false;
for (index, migration) in migrations.iter().enumerate() {
let migration =
sqlformat::format(migration, &sqlformat::QueryParams::None, Default::default());
if let Some((_, _, completed_migration)) = completed_migrations.get(index) {
// Reformat completed migrations with the current `sqlformat` version, so that past migrations stored
// conform to the new formatting rules.
let completed_migration = sqlformat::format(
completed_migration,
&sqlformat::QueryParams::None,
Default::default(),
);
if completed_migration == migration {
// Migration already run. Continue
continue;
} else if should_allow_migration_change(index, &completed_migration, &migration)
{
continue;
} else {
anyhow::bail!(formatdoc! {"
Migration changed for {domain} at step {index}
Stored migration:
{completed_migration}
Proposed migration:
{migration}"});
}
}
self.eager_exec(&migration)?;
did_migrate = true;
store_completed_migration((domain, index, migration))?;
}
if did_migrate {
self.delete_rows_with_orphaned_foreign_key_references()?;
self.exec("PRAGMA foreign_key_check;")?()?;
}
Ok(())
})
}
/// Delete any rows that were orphaned by a migration. This is needed
/// because we disable foreign key constraints during migrations, so
/// that it's possible to re-create a table with the same name, without
/// deleting all associated data.
fn delete_rows_with_orphaned_foreign_key_references(&self) -> Result<()> {
let foreign_key_info: Vec<(String, String, String, String)> = self.select(
r#"
SELECT DISTINCT
schema.name as child_table,
foreign_keys.[from] as child_key,
foreign_keys.[table] as parent_table,
foreign_keys.[to] as parent_key
FROM sqlite_schema schema
JOIN pragma_foreign_key_list(schema.name) foreign_keys
WHERE
schema.type = 'table' AND
schema.name NOT LIKE "sqlite_%"
"#,
)?()?;
if !foreign_key_info.is_empty() {
log::info!(
"Found {} foreign key relationships to check",
foreign_key_info.len()
);
}
for (child_table, child_key, parent_table, parent_key) in foreign_key_info {
self.exec(&format!(
"
DELETE FROM {child_table}
WHERE {child_key} IS NOT NULL and {child_key} NOT IN
(SELECT {parent_key} FROM {parent_table})
"
))?()?;
}
Ok(())
}
}
#[cfg(test)]
mod test {
use indoc::indoc;
use crate::connection::Connection;
#[test]
fn test_migrations_are_added_to_table() {
let connection = Connection::open_memory(Some("migrations_are_added_to_table"));
// Create first migration with a single step and run it
connection
.migrate(
"test",
&[indoc! {"
CREATE TABLE test1 (
a TEXT,
b TEXT
)"}],
&mut disallow_migration_change,
)
.unwrap();
// Verify it got added to the migrations table
assert_eq!(
&connection
.select::<String>("SELECT (migration) FROM migrations")
.unwrap()()
.unwrap()[..],
&[indoc! {"CREATE TABLE test1 (a TEXT, b TEXT)"}],
);
// Add another step to the migration and run it again
connection
.migrate(
"test",
&[
indoc! {"
CREATE TABLE test1 (
a TEXT,
b TEXT
)"},
indoc! {"
CREATE TABLE test2 (
c TEXT,
d TEXT
)"},
],
&mut disallow_migration_change,
)
.unwrap();
// Verify it is also added to the migrations table
assert_eq!(
&connection
.select::<String>("SELECT (migration) FROM migrations")
.unwrap()()
.unwrap()[..],
&[
indoc! {"CREATE TABLE test1 (a TEXT, b TEXT)"},
indoc! {"CREATE TABLE test2 (c TEXT, d TEXT)"},
],
);
}
#[test]
fn test_migration_setup_works() {
let connection = Connection::open_memory(Some("migration_setup_works"));
connection
.exec(indoc! {"
CREATE TABLE IF NOT EXISTS migrations (
domain TEXT,
step INTEGER,
migration TEXT
);"})
.unwrap()()
.unwrap();
let mut store_completed_migration = connection
.exec_bound::<(&str, usize, String)>(indoc! {"
INSERT INTO migrations (domain, step, migration)
VALUES (?, ?, ?)"})
.unwrap();
let domain = "test_domain";
for i in 0..5 {
// Create a table forcing a schema change
connection
.exec(&format!("CREATE TABLE table{} ( test TEXT );", i))
.unwrap()()
.unwrap();
store_completed_migration((domain, i, i.to_string())).unwrap();
}
}
#[test]
fn migrations_dont_rerun() {
let connection = Connection::open_memory(Some("migrations_dont_rerun"));
// Create migration which clears a table
// Manually create the table for that migration with a row
connection
.exec(indoc! {"
CREATE TABLE test_table (
test_column INTEGER
);"})
.unwrap()()
.unwrap();
connection
.exec(indoc! {"
INSERT INTO test_table (test_column) VALUES (1);"})
.unwrap()()
.unwrap();
assert_eq!(
connection
.select_row::<usize>("SELECT * FROM test_table")
.unwrap()()
.unwrap(),
Some(1)
);
// Run the migration verifying that the row got dropped
connection
.migrate(
"test",
&["DELETE FROM test_table"],
&mut disallow_migration_change,
)
.unwrap();
assert_eq!(
connection
.select_row::<usize>("SELECT * FROM test_table")
.unwrap()()
.unwrap(),
None
);
// Recreate the dropped row
connection
.exec("INSERT INTO test_table (test_column) VALUES (2)")
.unwrap()()
.unwrap();
// Run the same migration again and verify that the table was left unchanged
connection
.migrate(
"test",
&["DELETE FROM test_table"],
&mut disallow_migration_change,
)
.unwrap();
assert_eq!(
connection
.select_row::<usize>("SELECT * FROM test_table")
.unwrap()()
.unwrap(),
Some(2)
);
}
#[test]
fn changed_migration_fails() {
let connection = Connection::open_memory(Some("changed_migration_fails"));
// Create a migration with two steps and run it
connection
.migrate(
"test migration",
&[
"CREATE TABLE test (col INTEGER)",
"INSERT INTO test (col) VALUES (1)",
],
&mut disallow_migration_change,
)
.unwrap();
let mut migration_changed = false;
// Create another migration with the same domain but different steps
let second_migration_result = connection.migrate(
"test migration",
&[
"CREATE TABLE test (color INTEGER )",
"INSERT INTO test (color) VALUES (1)",
],
&mut |_, old, new| {
assert_eq!(old, "CREATE TABLE test (col INTEGER)");
assert_eq!(new, "CREATE TABLE test (color INTEGER)");
migration_changed = true;
false
},
);
// Verify new migration returns error when run
assert!(second_migration_result.is_err())
}
#[test]
fn test_create_alter_drop() {
let connection = Connection::open_memory(Some("test_create_alter_drop"));
connection
.migrate(
"first_migration",
&["CREATE TABLE table1(a TEXT) STRICT;"],
&mut disallow_migration_change,
)
.unwrap();
connection
.exec("INSERT INTO table1(a) VALUES (\"test text\");")
.unwrap()()
.unwrap();
connection
.migrate(
"second_migration",
&[indoc! {"
CREATE TABLE table2(b TEXT) STRICT;
INSERT INTO table2 (b)
SELECT a FROM table1;
DROP TABLE table1;
ALTER TABLE table2 RENAME TO table1;
"}],
&mut disallow_migration_change,
)
.unwrap();
let res = &connection.select::<String>("SELECT b FROM table1").unwrap()().unwrap()[0];
assert_eq!(res, "test text");
}
fn disallow_migration_change(_: usize, _: &str, _: &str) -> bool {
false
}
}

View File

@@ -0,0 +1,150 @@
use anyhow::Result;
use indoc::formatdoc;
use crate::connection::Connection;
impl Connection {
// Run a set of commands within the context of a `SAVEPOINT name`. If the callback
// returns Err(_), the savepoint will be rolled back. Otherwise, the save
// point is released.
pub fn with_savepoint<R, F>(&self, name: impl AsRef<str>, f: F) -> Result<R>
where
F: FnOnce() -> Result<R>,
{
let name = name.as_ref();
self.exec(&format!("SAVEPOINT {name}"))?()?;
let result = f();
match result {
Ok(_) => {
self.exec(&format!("RELEASE {name}"))?()?;
}
Err(_) => {
self.exec(&formatdoc! {"
ROLLBACK TO {name};
RELEASE {name}"})?()?;
}
}
result
}
// Run a set of commands within the context of a `SAVEPOINT name`. If the callback
// returns Ok(None) or Err(_), the savepoint will be rolled back. Otherwise, the save
// point is released.
pub fn with_savepoint_rollback<R, F>(&self, name: impl AsRef<str>, f: F) -> Result<Option<R>>
where
F: FnOnce() -> Result<Option<R>>,
{
let name = name.as_ref();
self.exec(&format!("SAVEPOINT {name}"))?()?;
let result = f();
match result {
Ok(Some(_)) => {
self.exec(&format!("RELEASE {name}"))?()?;
}
Ok(None) | Err(_) => {
self.exec(&formatdoc! {"
ROLLBACK TO {name};
RELEASE {name}"})?()?;
}
}
result
}
}
#[cfg(test)]
mod tests {
use crate::connection::Connection;
use anyhow::Result;
use indoc::indoc;
#[test]
fn test_nested_savepoints() -> Result<()> {
let connection = Connection::open_memory(Some("nested_savepoints"));
connection
.exec(indoc! {"
CREATE TABLE text (
text TEXT,
idx INTEGER
);"})
.unwrap()()
.unwrap();
let save1_text = "test save1";
let save2_text = "test save2";
connection.with_savepoint("first", || {
connection.exec_bound("INSERT INTO text(text, idx) VALUES (?, ?)")?((save1_text, 1))?;
assert!(
connection
.with_savepoint("second", || -> anyhow::Result<Option<()>> {
connection.exec_bound("INSERT INTO text(text, idx) VALUES (?, ?)")?((
save2_text, 2,
))?;
assert_eq!(
connection
.select::<String>("SELECT text FROM text ORDER BY text.idx ASC")?(
)?,
vec![save1_text, save2_text],
);
anyhow::bail!("Failed second save point :(")
})
.err()
.is_some()
);
assert_eq!(
connection.select::<String>("SELECT text FROM text ORDER BY text.idx ASC")?()?,
vec![save1_text],
);
connection.with_savepoint_rollback::<(), _>("second", || {
connection.exec_bound("INSERT INTO text(text, idx) VALUES (?, ?)")?((
save2_text, 2,
))?;
assert_eq!(
connection.select::<String>("SELECT text FROM text ORDER BY text.idx ASC")?()?,
vec![save1_text, save2_text],
);
Ok(None)
})?;
assert_eq!(
connection.select::<String>("SELECT text FROM text ORDER BY text.idx ASC")?()?,
vec![save1_text],
);
connection.with_savepoint_rollback("second", || {
connection.exec_bound("INSERT INTO text(text, idx) VALUES (?, ?)")?((
save2_text, 2,
))?;
assert_eq!(
connection.select::<String>("SELECT text FROM text ORDER BY text.idx ASC")?()?,
vec![save1_text, save2_text],
);
Ok(Some(()))
})?;
assert_eq!(
connection.select::<String>("SELECT text FROM text ORDER BY text.idx ASC")?()?,
vec![save1_text, save2_text],
);
Ok(())
})?;
assert_eq!(
connection.select::<String>("SELECT text FROM text ORDER BY text.idx ASC")?()?,
vec![save1_text, save2_text],
);
Ok(())
}
}

View File

@@ -0,0 +1,497 @@
use std::ffi::{CStr, CString, c_int};
use std::marker::PhantomData;
use std::{ptr, slice, str};
use anyhow::{Context as _, Result, bail};
use libsqlite3_sys::*;
use crate::bindable::{Bind, Column};
use crate::connection::Connection;
pub struct Statement<'a> {
/// vector of pointers to the raw SQLite statement objects.
/// it holds the actual prepared statements that will be executed.
pub raw_statements: Vec<*mut sqlite3_stmt>,
/// Index of the current statement being executed from the `raw_statements` vector.
current_statement: usize,
/// A reference to the database connection.
/// This is used to execute the statements and check for errors.
connection: &'a Connection,
///Indicates that the `Statement` struct is tied to the lifetime of the SQLite statement
phantom: PhantomData<sqlite3_stmt>,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum StepResult {
Row,
Done,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum SqlType {
Text,
Integer,
Blob,
Float,
Null,
}
impl<'a> Statement<'a> {
pub fn prepare<T: AsRef<str>>(connection: &'a Connection, query: T) -> Result<Self> {
let mut statement = Self {
raw_statements: Default::default(),
current_statement: 0,
connection,
phantom: PhantomData,
};
let sql = CString::new(query.as_ref()).context("Error creating cstr")?;
let mut remaining_sql = sql.as_c_str();
while {
let remaining_sql_str = remaining_sql
.to_str()
.context("Parsing remaining sql")?
.trim();
remaining_sql_str != ";" && !remaining_sql_str.is_empty()
} {
let mut raw_statement = ptr::null_mut::<sqlite3_stmt>();
let mut remaining_sql_ptr = ptr::null();
unsafe {
sqlite3_prepare_v2(
connection.sqlite3,
remaining_sql.as_ptr(),
-1,
&mut raw_statement,
&mut remaining_sql_ptr,
)
};
connection
.last_error()
.with_context(|| format!("Prepare call failed for query:\n{}", query.as_ref()))?;
remaining_sql = unsafe { CStr::from_ptr(remaining_sql_ptr) };
statement.raw_statements.push(raw_statement);
if !connection.can_write() && unsafe { sqlite3_stmt_readonly(raw_statement) == 0 } {
let sql = unsafe { CStr::from_ptr(sqlite3_sql(raw_statement)) };
bail!(
"Write statement prepared with connection that is not write capable. SQL:\n{} ",
sql.to_str()?
)
}
}
Ok(statement)
}
fn current_statement(&self) -> *mut sqlite3_stmt {
*self.raw_statements.get(self.current_statement).unwrap()
}
pub fn reset(&mut self) {
unsafe {
for raw_statement in self.raw_statements.iter() {
sqlite3_reset(*raw_statement);
}
}
self.current_statement = 0;
}
pub fn parameter_count(&self) -> i32 {
unsafe {
self.raw_statements
.iter()
.map(|raw_statement| sqlite3_bind_parameter_count(*raw_statement))
.max()
.unwrap_or(0)
}
}
fn bind_index_with(&self, index: i32, bind: &dyn Fn(&*mut sqlite3_stmt)) -> Result<()> {
let mut any_succeed = false;
unsafe {
for raw_statement in self.raw_statements.iter() {
if index <= sqlite3_bind_parameter_count(*raw_statement) {
bind(raw_statement);
self.connection
.last_error()
.with_context(|| format!("Failed to bind value at index {index}"))?;
any_succeed = true;
} else {
continue;
}
}
}
if any_succeed {
Ok(())
} else {
anyhow::bail!("Failed to bind parameters")
}
}
pub fn bind_blob(&self, index: i32, blob: &[u8]) -> Result<()> {
let index = index as c_int;
let blob_pointer = blob.as_ptr() as *const _;
let len = blob.len() as c_int;
self.bind_index_with(index, &|raw_statement| unsafe {
sqlite3_bind_blob(*raw_statement, index, blob_pointer, len, SQLITE_TRANSIENT());
})
}
pub fn column_blob(&mut self, index: i32) -> Result<&[u8]> {
let index = index as c_int;
let pointer = unsafe { sqlite3_column_blob(self.current_statement(), index) };
self.connection
.last_error()
.with_context(|| format!("Failed to read blob at index {index}"))?;
if pointer.is_null() {
return Ok(&[]);
}
let len = unsafe { sqlite3_column_bytes(self.current_statement(), index) as usize };
self.connection
.last_error()
.with_context(|| format!("Failed to read length of blob at index {index}"))?;
unsafe { Ok(slice::from_raw_parts(pointer as *const u8, len)) }
}
pub fn bind_double(&self, index: i32, double: f64) -> Result<()> {
let index = index as c_int;
self.bind_index_with(index, &|raw_statement| unsafe {
sqlite3_bind_double(*raw_statement, index, double);
})
}
pub fn column_double(&self, index: i32) -> Result<f64> {
let index = index as c_int;
let result = unsafe { sqlite3_column_double(self.current_statement(), index) };
self.connection
.last_error()
.with_context(|| format!("Failed to read double at index {index}"))?;
Ok(result)
}
pub fn bind_int(&self, index: i32, int: i32) -> Result<()> {
let index = index as c_int;
self.bind_index_with(index, &|raw_statement| unsafe {
sqlite3_bind_int(*raw_statement, index, int);
})
}
pub fn column_int(&self, index: i32) -> Result<i32> {
let index = index as c_int;
let result = unsafe { sqlite3_column_int(self.current_statement(), index) };
self.connection
.last_error()
.with_context(|| format!("Failed to read int at index {index}"))?;
Ok(result)
}
pub fn bind_int64(&self, index: i32, int: i64) -> Result<()> {
let index = index as c_int;
self.bind_index_with(index, &|raw_statement| unsafe {
sqlite3_bind_int64(*raw_statement, index, int);
})
}
pub fn column_int64(&self, index: i32) -> Result<i64> {
let index = index as c_int;
let result = unsafe { sqlite3_column_int64(self.current_statement(), index) };
self.connection
.last_error()
.with_context(|| format!("Failed to read i64 at index {index}"))?;
Ok(result)
}
pub fn bind_null(&self, index: i32) -> Result<()> {
let index = index as c_int;
self.bind_index_with(index, &|raw_statement| unsafe {
sqlite3_bind_null(*raw_statement, index);
})
}
pub fn bind_text(&self, index: i32, text: &str) -> Result<()> {
let index = index as c_int;
let text_pointer = text.as_ptr() as *const _;
let len = text.len() as c_int;
self.bind_index_with(index, &|raw_statement| unsafe {
sqlite3_bind_text(*raw_statement, index, text_pointer, len, SQLITE_TRANSIENT());
})
}
pub fn column_text(&mut self, index: i32) -> Result<&str> {
let index = index as c_int;
let pointer = unsafe { sqlite3_column_text(self.current_statement(), index) };
self.connection
.last_error()
.with_context(|| format!("Failed to read text from column {index}"))?;
if pointer.is_null() {
return Ok("");
}
let len = unsafe { sqlite3_column_bytes(self.current_statement(), index) as usize };
self.connection
.last_error()
.with_context(|| format!("Failed to read text length at {index}"))?;
let slice = unsafe { slice::from_raw_parts(pointer, len) };
Ok(str::from_utf8(slice)?)
}
pub fn bind<T: Bind>(&self, value: &T, index: i32) -> Result<i32> {
debug_assert!(index > 0);
value.bind(self, index)
}
pub fn column<T: Column>(&mut self) -> Result<T> {
Ok(T::column(self, 0)?.0)
}
pub fn column_type(&mut self, index: i32) -> Result<SqlType> {
let result = unsafe { sqlite3_column_type(self.current_statement(), index) };
self.connection.last_error()?;
match result {
SQLITE_INTEGER => Ok(SqlType::Integer),
SQLITE_FLOAT => Ok(SqlType::Float),
SQLITE_TEXT => Ok(SqlType::Text),
SQLITE_BLOB => Ok(SqlType::Blob),
SQLITE_NULL => Ok(SqlType::Null),
_ => anyhow::bail!("Column type returned was incorrect"),
}
}
pub fn with_bindings(&mut self, bindings: &impl Bind) -> Result<&mut Self> {
self.bind(bindings, 1)?;
Ok(self)
}
fn step(&mut self) -> Result<StepResult> {
match unsafe { sqlite3_step(self.current_statement()) } {
SQLITE_ROW => Ok(StepResult::Row),
SQLITE_DONE => {
if self.current_statement >= self.raw_statements.len() - 1 {
Ok(StepResult::Done)
} else {
self.current_statement += 1;
self.step()
}
}
SQLITE_MISUSE => anyhow::bail!("Statement step returned SQLITE_MISUSE"),
_other_error => {
self.connection.last_error()?;
unreachable!("Step returned error code and last error failed to catch it");
}
}
}
pub fn exec(&mut self) -> Result<()> {
fn logic(this: &mut Statement) -> Result<()> {
while this.step()? == StepResult::Row {}
Ok(())
}
let result = logic(self);
self.reset();
result
}
pub fn map<R>(&mut self, callback: impl FnMut(&mut Statement) -> Result<R>) -> Result<Vec<R>> {
fn logic<R>(
this: &mut Statement,
mut callback: impl FnMut(&mut Statement) -> Result<R>,
) -> Result<Vec<R>> {
let mut mapped_rows = Vec::new();
while this.step()? == StepResult::Row {
mapped_rows.push(callback(this)?);
}
Ok(mapped_rows)
}
let result = logic(self, callback);
self.reset();
result
}
pub fn rows<R: Column>(&mut self) -> Result<Vec<R>> {
self.map(|s| s.column::<R>())
}
pub fn single<R>(&mut self, callback: impl FnOnce(&mut Statement) -> Result<R>) -> Result<R> {
fn logic<R>(
this: &mut Statement,
callback: impl FnOnce(&mut Statement) -> Result<R>,
) -> Result<R> {
println!("{:?}", std::any::type_name::<R>());
anyhow::ensure!(
this.step()? == StepResult::Row,
"single called with query that returns no rows."
);
let result = callback(this)?;
anyhow::ensure!(
this.step()? == StepResult::Done,
"single called with a query that returns more than one row."
);
Ok(result)
}
let result = logic(self, callback);
self.reset();
result
}
pub fn row<R: Column>(&mut self) -> Result<R> {
self.single(|this| this.column::<R>())
}
pub fn maybe<R>(
&mut self,
callback: impl FnOnce(&mut Statement) -> Result<R>,
) -> Result<Option<R>> {
fn logic<R>(
this: &mut Statement,
callback: impl FnOnce(&mut Statement) -> Result<R>,
) -> Result<Option<R>> {
if this.step().context("Failed on step call")? != StepResult::Row {
return Ok(None);
}
let result = callback(this)
.map(|r| Some(r))
.context("Failed to parse row result")?;
anyhow::ensure!(
this.step().context("Second step call")? == StepResult::Done,
"maybe called with a query that returns more than one row."
);
Ok(result)
}
let result = logic(self, callback);
self.reset();
result
}
pub fn maybe_row<R: Column>(&mut self) -> Result<Option<R>> {
self.maybe(|this| this.column::<R>())
}
}
impl Drop for Statement<'_> {
fn drop(&mut self) {
unsafe {
for raw_statement in self.raw_statements.iter() {
sqlite3_finalize(*raw_statement);
}
}
}
}
#[cfg(test)]
mod test {
use indoc::indoc;
use crate::{
connection::Connection,
statement::{Statement, StepResult},
};
#[test]
fn binding_multiple_statements_with_parameter_gaps() {
let connection =
Connection::open_memory(Some("binding_multiple_statements_with_parameter_gaps"));
connection
.exec(indoc! {"
CREATE TABLE test (
col INTEGER
)"})
.unwrap()()
.unwrap();
let statement = Statement::prepare(
&connection,
indoc! {"
INSERT INTO test(col) VALUES (?3);
SELECT * FROM test WHERE col = ?1"},
)
.unwrap();
statement
.bind_int(1, 1)
.expect("Could not bind parameter to first index");
statement
.bind_int(2, 2)
.expect("Could not bind parameter to second index");
statement
.bind_int(3, 3)
.expect("Could not bind parameter to third index");
}
#[test]
fn blob_round_trips() {
let connection1 = Connection::open_memory(Some("blob_round_trips"));
connection1
.exec(indoc! {"
CREATE TABLE blobs (
data BLOB
)"})
.unwrap()()
.unwrap();
let blob = &[0, 1, 2, 4, 8, 16, 32, 64];
let mut write =
Statement::prepare(&connection1, "INSERT INTO blobs (data) VALUES (?)").unwrap();
write.bind_blob(1, blob).unwrap();
assert_eq!(write.step().unwrap(), StepResult::Done);
// Read the blob from the
let connection2 = Connection::open_memory(Some("blob_round_trips"));
let mut read = Statement::prepare(&connection2, "SELECT * FROM blobs").unwrap();
assert_eq!(read.step().unwrap(), StepResult::Row);
assert_eq!(read.column_blob(0).unwrap(), blob);
assert_eq!(read.step().unwrap(), StepResult::Done);
// Delete the added blob and verify its deleted on the other side
connection2.exec("DELETE FROM blobs").unwrap()().unwrap();
let mut read = Statement::prepare(&connection1, "SELECT * FROM blobs").unwrap();
assert_eq!(read.step().unwrap(), StepResult::Done);
}
#[test]
pub fn maybe_returns_options() {
let connection = Connection::open_memory(Some("maybe_returns_options"));
connection
.exec(indoc! {"
CREATE TABLE texts (
text TEXT
)"})
.unwrap()()
.unwrap();
assert!(
connection
.select_row::<String>("SELECT text FROM texts")
.unwrap()()
.unwrap()
.is_none()
);
let text_to_insert = "This is a test";
connection
.exec_bound("INSERT INTO texts VALUES (?)")
.unwrap()(text_to_insert)
.unwrap();
assert_eq!(
connection.select_row("SELECT text FROM texts").unwrap()().unwrap(),
Some(text_to_insert.to_string())
);
}
}

View File

@@ -0,0 +1,374 @@
use anyhow::Context as _;
use collections::HashMap;
use futures::{Future, FutureExt, channel::oneshot};
use parking_lot::{Mutex, RwLock};
use std::{
marker::PhantomData,
ops::Deref,
sync::{Arc, LazyLock},
thread,
time::Duration,
};
use thread_local::ThreadLocal;
use crate::{connection::Connection, domain::Migrator, util::UnboundedSyncSender};
const MIGRATION_RETRIES: usize = 10;
const CONNECTION_INITIALIZE_RETRIES: usize = 50;
const CONNECTION_INITIALIZE_RETRY_DELAY: Duration = Duration::from_millis(1);
type QueuedWrite = Box<dyn 'static + Send + FnOnce()>;
type WriteQueue = Box<dyn 'static + Send + Sync + Fn(QueuedWrite)>;
type WriteQueueConstructor = Box<dyn 'static + Send + FnMut() -> WriteQueue>;
/// List of queues of tasks by database uri. This lets us serialize writes to the database
/// and have a single worker thread per db file. This means many thread safe connections
/// (possibly with different migrations) could all be communicating with the same background
/// thread.
static QUEUES: LazyLock<RwLock<HashMap<Arc<str>, WriteQueue>>> = LazyLock::new(Default::default);
/// Thread safe connection to a given database file or in memory db. This can be cloned, shared, static,
/// whatever. It derefs to a synchronous connection by thread that is read only. A write capable connection
/// may be accessed by passing a callback to the `write` function which will queue the callback
#[derive(Clone)]
pub struct ThreadSafeConnection {
uri: Arc<str>,
persistent: bool,
connection_initialize_query: Option<&'static str>,
connections: Arc<ThreadLocal<Connection>>,
}
unsafe impl Send for ThreadSafeConnection {}
unsafe impl Sync for ThreadSafeConnection {}
pub struct ThreadSafeConnectionBuilder<M: Migrator + 'static = ()> {
db_initialize_query: Option<&'static str>,
write_queue_constructor: Option<WriteQueueConstructor>,
connection: ThreadSafeConnection,
_migrator: PhantomData<*mut M>,
}
impl<M: Migrator> ThreadSafeConnectionBuilder<M> {
/// Sets the query to run every time a connection is opened. This must
/// be infallible (EG only use pragma statements) and not cause writes.
/// to the db or it will panic.
pub fn with_connection_initialize_query(mut self, initialize_query: &'static str) -> Self {
self.connection.connection_initialize_query = Some(initialize_query);
self
}
/// Queues an initialization query for the database file. This must be infallible
/// but may cause changes to the database file such as with `PRAGMA journal_mode`
pub fn with_db_initialization_query(mut self, initialize_query: &'static str) -> Self {
self.db_initialize_query = Some(initialize_query);
self
}
/// Specifies how the thread safe connection should serialize writes. If provided
/// the connection will call the write_queue_constructor for each database file in
/// this process. The constructor is responsible for setting up a background thread or
/// async task which handles queued writes with the provided connection.
pub fn with_write_queue_constructor(
mut self,
write_queue_constructor: WriteQueueConstructor,
) -> Self {
self.write_queue_constructor = Some(write_queue_constructor);
self
}
pub async fn build(self) -> anyhow::Result<ThreadSafeConnection> {
self.connection
.initialize_queues(self.write_queue_constructor);
let db_initialize_query = self.db_initialize_query;
self.connection
.write(move |connection| {
if let Some(db_initialize_query) = db_initialize_query {
connection.exec(db_initialize_query).with_context(|| {
format!(
"Db initialize query failed to execute: {}",
db_initialize_query
)
})?()?;
}
// Retry failed migrations in case they were run in parallel from different
// processes. This gives a best attempt at migrating before bailing
let mut migration_result =
anyhow::Result::<()>::Err(anyhow::anyhow!("Migration never run"));
let foreign_keys_enabled: bool =
connection.select_row::<i32>("PRAGMA foreign_keys")?()
.unwrap_or(None)
.map(|enabled| enabled != 0)
.unwrap_or(false);
connection.exec("PRAGMA foreign_keys = OFF;")?()?;
for _ in 0..MIGRATION_RETRIES {
migration_result = connection
.with_savepoint("thread_safe_multi_migration", || M::migrate(connection));
if migration_result.is_ok() {
break;
}
}
if foreign_keys_enabled {
connection.exec("PRAGMA foreign_keys = ON;")?()?;
}
migration_result
})
.await?;
Ok(self.connection)
}
}
impl ThreadSafeConnection {
fn initialize_queues(&self, write_queue_constructor: Option<WriteQueueConstructor>) -> bool {
if !QUEUES.read().contains_key(&self.uri) {
let mut queues = QUEUES.write();
if !queues.contains_key(&self.uri) {
let mut write_queue_constructor =
write_queue_constructor.unwrap_or_else(background_thread_queue);
queues.insert(self.uri.clone(), write_queue_constructor());
return true;
}
}
false
}
pub fn builder<M: Migrator>(uri: &str, persistent: bool) -> ThreadSafeConnectionBuilder<M> {
ThreadSafeConnectionBuilder::<M> {
db_initialize_query: None,
write_queue_constructor: None,
connection: Self {
uri: Arc::from(uri),
persistent,
connection_initialize_query: None,
connections: Default::default(),
},
_migrator: PhantomData,
}
}
/// Opens a new db connection with the initialized file path. This is internal and only
/// called from the deref function.
fn open_file(uri: &str) -> Connection {
Connection::open_file(uri)
}
/// Opens a shared memory connection using the file path as the identifier. This is internal
/// and only called from the deref function.
fn open_shared_memory(uri: &str) -> Connection {
Connection::open_memory(Some(uri))
}
pub fn write<T: 'static + Send + Sync>(
&self,
callback: impl 'static + Send + FnOnce(&Connection) -> T,
) -> impl Future<Output = T> {
// Check and invalidate queue and maybe recreate queue
let queues = QUEUES.read();
let write_channel = queues
.get(&self.uri)
.expect("Queues are inserted when build is called. This should always succeed");
// Create a one shot channel for the result of the queued write
// so we can await on the result
let (sender, receiver) = oneshot::channel();
let thread_safe_connection = (*self).clone();
write_channel(Box::new(move || {
let connection = thread_safe_connection.deref();
let result = connection.with_write(|connection| callback(connection));
sender.send(result).ok();
}));
receiver.map(|response| response.expect("Write queue unexpectedly closed"))
}
pub(crate) fn create_connection(
persistent: bool,
uri: &str,
connection_initialize_query: Option<&'static str>,
) -> Connection {
let mut connection = if persistent {
Self::open_file(uri)
} else {
Self::open_shared_memory(uri)
};
if let Some(initialize_query) = connection_initialize_query {
let mut last_error = None;
let initialized = (0..CONNECTION_INITIALIZE_RETRIES).any(|attempt| {
match connection
.exec(initialize_query)
.and_then(|mut statement| statement())
{
Ok(()) => true,
Err(err)
if is_schema_lock_error(&err)
&& attempt + 1 < CONNECTION_INITIALIZE_RETRIES =>
{
last_error = Some(err);
thread::sleep(CONNECTION_INITIALIZE_RETRY_DELAY);
false
}
Err(err) => {
panic!(
"Initialize query failed to execute: {}\n\nCaused by:\n{err:#}",
initialize_query
)
}
}
});
if !initialized {
let err = last_error
.expect("connection initialization retries should record the last error");
panic!(
"Initialize query failed to execute after retries: {}\n\nCaused by:\n{err:#}",
initialize_query
);
}
}
// Disallow writes on the connection. The only writes allowed for thread safe connections
// are from the background thread that can serialize them.
*connection.write.get_mut() = false;
connection
}
}
fn is_schema_lock_error(err: &anyhow::Error) -> bool {
let message = format!("{err:#}");
message.contains("database schema is locked") || message.contains("database is locked")
}
impl ThreadSafeConnection {
/// Special constructor for ThreadSafeConnection which disallows db initialization and migrations.
/// This allows construction to be infallible and not write to the db.
pub fn new(
uri: &str,
persistent: bool,
connection_initialize_query: Option<&'static str>,
write_queue_constructor: Option<WriteQueueConstructor>,
) -> Self {
let connection = Self {
uri: Arc::from(uri),
persistent,
connection_initialize_query,
connections: Default::default(),
};
connection.initialize_queues(write_queue_constructor);
connection
}
}
impl Deref for ThreadSafeConnection {
type Target = Connection;
fn deref(&self) -> &Self::Target {
self.connections.get_or(|| {
Self::create_connection(self.persistent, &self.uri, self.connection_initialize_query)
})
}
}
pub fn background_thread_queue() -> WriteQueueConstructor {
use std::sync::mpsc::channel;
Box::new(|| {
let (sender, receiver) = channel::<QueuedWrite>();
thread::Builder::new()
.name("sqlezWorker".to_string())
.spawn(move || {
while let Ok(write) = receiver.recv() {
write()
}
})
.unwrap();
let sender = UnboundedSyncSender::new(sender);
Box::new(move |queued_write| {
sender
.send(queued_write)
.expect("Could not send write action to background thread");
})
})
}
pub fn locking_queue() -> WriteQueueConstructor {
Box::new(|| {
let write_mutex = Mutex::new(());
Box::new(move |queued_write| {
let _lock = write_mutex.lock();
queued_write();
})
})
}
#[cfg(test)]
mod test {
use indoc::indoc;
use std::ops::Deref;
use std::{thread, time::Duration};
use crate::{domain::Domain, thread_safe_connection::ThreadSafeConnection};
#[test]
fn many_initialize_and_migrate_queries_at_once() {
let mut handles = vec![];
enum TestDomain {}
impl Domain for TestDomain {
const NAME: &str = "test";
const MIGRATIONS: &[&str] = &["CREATE TABLE test(col1 TEXT, col2 TEXT) STRICT;"];
}
for _ in 0..100 {
handles.push(thread::spawn(|| {
let builder =
ThreadSafeConnection::builder::<TestDomain>("annoying-test.db", false)
.with_db_initialization_query("PRAGMA journal_mode=WAL")
.with_connection_initialize_query(indoc! {"
PRAGMA synchronous=NORMAL;
PRAGMA busy_timeout=1;
PRAGMA foreign_keys=TRUE;
PRAGMA case_sensitive_like=TRUE;
"});
let _ = pollster::block_on(builder.build()).unwrap().deref();
}));
}
for handle in handles {
let _ = handle.join();
}
}
#[test]
fn connection_initialize_query_retries_transient_schema_lock() {
let name = "connection_initialize_query_retries_transient_schema_lock";
let locking_connection = crate::connection::Connection::open_memory(Some(name));
locking_connection.exec("BEGIN IMMEDIATE").unwrap()().unwrap();
locking_connection
.exec("CREATE TABLE test(col TEXT)")
.unwrap()()
.unwrap();
let releaser = thread::spawn(move || {
thread::sleep(Duration::from_millis(10));
locking_connection.exec("ROLLBACK").unwrap()().unwrap();
});
ThreadSafeConnection::create_connection(false, name, Some("PRAGMA FOREIGN_KEYS=true"));
releaser.join().unwrap();
}
}

View File

@@ -0,0 +1,96 @@
use anyhow::{Context as _, Result};
use crate::{
bindable::{Bind, Column},
connection::Connection,
statement::Statement,
};
impl Connection {
/// Prepare a statement which has no bindings and returns nothing.
///
/// Note: If there are multiple statements that depend upon each other
/// (such as those which make schema changes), preparation will fail.
/// Use a true migration instead.
pub fn exec<'a>(&'a self, query: &str) -> Result<impl 'a + FnMut() -> Result<()>> {
let mut statement = Statement::prepare(self, query)?;
Ok(move || statement.exec())
}
/// Prepare a statement which takes a binding, but returns nothing.
/// The bindings for a given invocation should be passed to the returned
/// closure
///
/// Note: If there are multiple statements that depend upon each other
/// (such as those which make schema changes), preparation will fail.
/// Use a true migration instead.
pub fn exec_bound<'a, B: Bind>(
&'a self,
query: &str,
) -> Result<impl 'a + FnMut(B) -> Result<()>> {
let mut statement = Statement::prepare(self, query)?;
Ok(move |bindings| statement.with_bindings(&bindings)?.exec())
}
/// Prepare a statement which has no bindings and returns a `Vec<C>`.
///
/// Note: If there are multiple statements that depend upon each other
/// (such as those which make schema changes), preparation will fail.
/// Use a true migration instead.
pub fn select<'a, C: Column>(
&'a self,
query: &str,
) -> Result<impl 'a + FnMut() -> Result<Vec<C>>> {
let mut statement = Statement::prepare(self, query)?;
Ok(move || statement.rows::<C>())
}
/// Prepare a statement which takes a binding and returns a `Vec<C>`.
///
/// Note: If there are multiple statements that depend upon each other
/// (such as those which make schema changes), preparation will fail.
/// Use a true migration instead.
pub fn select_bound<'a, B: Bind, C: Column>(
&'a self,
query: &str,
) -> Result<impl 'a + FnMut(B) -> Result<Vec<C>>> {
let mut statement = Statement::prepare(self, query)?;
Ok(move |bindings| statement.with_bindings(&bindings)?.rows::<C>())
}
/// Prepare a statement that selects a single row from the database.
/// Will return none if no rows are returned and will error if more than
/// 1 row
///
/// Note: If there are multiple statements that depend upon each other
/// (such as those which make schema changes), preparation will fail.
/// Use a true migration instead.
pub fn select_row<'a, C: Column>(
&'a self,
query: &str,
) -> Result<impl 'a + FnMut() -> Result<Option<C>>> {
let mut statement = Statement::prepare(self, query)?;
Ok(move || statement.maybe_row::<C>())
}
/// Prepare a statement which takes a binding and selects a single row
/// from the database. Will return none if no rows are returned and will
/// error if more than 1 row is returned.
///
/// Note: If there are multiple statements that depend upon each other
/// (such as those which make schema changes), preparation will fail.
/// Use a true migration instead.
pub fn select_row_bound<'a, B: Bind, C: Column>(
&'a self,
query: &str,
) -> Result<impl 'a + FnMut(B) -> Result<Option<C>>> {
let mut statement = Statement::prepare(self, query)?;
Ok(move |bindings| {
statement
.with_bindings(&bindings)
.context("Bindings failed")?
.maybe_row::<C>()
.context("Maybe row failed")
})
}
}

32
crates/sqlez/src/util.rs Normal file
View File

@@ -0,0 +1,32 @@
use std::ops::Deref;
use std::sync::mpsc::Sender;
use parking_lot::Mutex;
use thread_local::ThreadLocal;
/// Unbounded standard library sender which is stored per thread to get around
/// the lack of sync on the standard library version while still being unbounded
/// Note: this locks on the cloneable sender, but its done once per thread, so it
/// shouldn't result in too much contention
pub struct UnboundedSyncSender<T: Send> {
cloneable_sender: Mutex<Sender<T>>,
local_senders: ThreadLocal<Sender<T>>,
}
impl<T: Send> UnboundedSyncSender<T> {
pub fn new(sender: Sender<T>) -> Self {
Self {
cloneable_sender: Mutex::new(sender),
local_senders: ThreadLocal::new(),
}
}
}
impl<T: Send> Deref for UnboundedSyncSender<T> {
type Target = Sender<T>;
fn deref(&self) -> &Self::Target {
self.local_senders
.get_or(|| self.cloneable_sender.lock().clone())
}
}