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:
2
crates/sqlez/.gitignore
vendored
Normal file
2
crates/sqlez/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
debug/
|
||||
target/
|
||||
23
crates/sqlez/Cargo.toml
Normal file
23
crates/sqlez/Cargo.toml
Normal 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
1
crates/sqlez/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
466
crates/sqlez/src/bindable.rs
Normal file
466
crates/sqlez/src/bindable.rs
Normal 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
|
||||
);
|
||||
532
crates/sqlez/src/connection.rs
Normal file
532
crates/sqlez/src/connection.rs
Normal 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()
|
||||
);
|
||||
}
|
||||
}
|
||||
64
crates/sqlez/src/domain.rs
Normal file
64
crates/sqlez/src/domain.rs
Normal 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
11
crates/sqlez/src/lib.rs
Normal 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;
|
||||
390
crates/sqlez/src/migrations.rs
Normal file
390
crates/sqlez/src/migrations.rs
Normal 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
|
||||
}
|
||||
}
|
||||
150
crates/sqlez/src/savepoint.rs
Normal file
150
crates/sqlez/src/savepoint.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
497
crates/sqlez/src/statement.rs
Normal file
497
crates/sqlez/src/statement.rs
Normal 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())
|
||||
);
|
||||
}
|
||||
}
|
||||
374
crates/sqlez/src/thread_safe_connection.rs
Normal file
374
crates/sqlez/src/thread_safe_connection.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
96
crates/sqlez/src/typed_statements.rs
Normal file
96
crates/sqlez/src/typed_statements.rs
Normal 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
32
crates/sqlez/src/util.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user