Detailed changes
@@ -1,26 +1,27 @@
pub mod kvp;
+pub mod query;
// Re-export
pub use anyhow;
use anyhow::Context;
pub use indoc::indoc;
pub use lazy_static;
+use parking_lot::{Mutex, RwLock};
pub use smol;
pub use sqlez;
pub use sqlez_macros;
+pub use util::channel::{RELEASE_CHANNEL, RELEASE_CHANNEL_NAME};
+pub use util::paths::DB_DIR;
use sqlez::domain::Migrator;
use sqlez::thread_safe_connection::ThreadSafeConnection;
use sqlez_macros::sql;
use std::fs::{create_dir_all, remove_dir_all};
-use std::path::Path;
+use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
use util::{async_iife, ResultExt};
-use util::channel::{ReleaseChannel, RELEASE_CHANNEL, RELEASE_CHANNEL_NAME};
-use util::paths::DB_DIR;
-
-// TODO: Add a savepoint to the thread safe connection initialization and migrations
+use util::channel::ReleaseChannel;
const CONNECTION_INITIALIZE_QUERY: &'static str = sql!(
PRAGMA synchronous=NORMAL;
@@ -36,79 +37,117 @@ const DB_INITIALIZE_QUERY: &'static str = sql!(
const FALLBACK_DB_NAME: &'static str = "FALLBACK_MEMORY_DB";
lazy_static::lazy_static! {
- static ref DB_WIPED: AtomicBool = AtomicBool::new(false);
+ static ref DB_FILE_OPERATIONS: Mutex<()> = Mutex::new(());
+ static ref DB_WIPED: RwLock<bool> = RwLock::new(false);
+ pub static ref BACKUP_DB_PATH: RwLock<Option<PathBuf>> = RwLock::new(None);
+ pub static ref ALL_FILE_DB_FAILED: AtomicBool = AtomicBool::new(false);
}
/// Open or create a database at the given directory path.
-pub async fn open_db<M: Migrator>() -> ThreadSafeConnection<M> {
- let db_dir = (*DB_DIR).join(Path::new(&format!("0-{}", *RELEASE_CHANNEL_NAME)));
+/// This will retry a couple times if there are failures. If opening fails once, the db directory
+/// is moved to a backup folder and a new one is created. If that fails, a shared in memory db is created.
+/// In either case, static variables are set so that the user can be notified.
+pub async fn open_db<M: Migrator + 'static>(wipe_db: bool, db_dir: &Path, release_channel: &ReleaseChannel) -> ThreadSafeConnection<M> {
+ let main_db_dir = db_dir.join(Path::new(&format!("0-{}", release_channel.name())));
// If WIPE_DB, delete 0-{channel}
- if *RELEASE_CHANNEL == ReleaseChannel::Dev
- && std::env::var("WIPE_DB").is_ok()
- && !DB_WIPED.load(Ordering::Acquire)
+ if release_channel == &ReleaseChannel::Dev
+ && wipe_db
+ && !*DB_WIPED.read()
{
- remove_dir_all(&db_dir).ok();
- DB_WIPED.store(true, Ordering::Release);
+ let mut db_wiped = DB_WIPED.write();
+ if !*db_wiped {
+ remove_dir_all(&main_db_dir).ok();
+
+ *db_wiped = true;
+ }
}
let connection = async_iife!({
+ // Note: This still has a race condition where 1 set of migrations succeeds
+ // (e.g. (Workspace, Editor)) and another fails (e.g. (Workspace, Terminal))
+ // This will cause the first connection to have the database taken out
+ // from under it. This *should* be fine though. The second dabatase failure will
+ // cause errors in the log and so should be observed by developers while writing
+ // soon-to-be good migrations. If user databases are corrupted, we toss them out
+ // and try again from a blank. As long as running all migrations from start to end
+ // is ok, this race condition will never be triggered.
+ //
+ // Basically: Don't ever push invalid migrations to stable or everyone will have
+ // a bad time.
+
// If no db folder, create one at 0-{channel}
- create_dir_all(&db_dir).context("Could not create db directory")?;
- let db_path = db_dir.join(Path::new("db.sqlite"));
-
- // Try building a connection
- if let Some(connection) = ThreadSafeConnection::<M>::builder(db_path.to_string_lossy().as_ref(), true)
- .with_db_initialization_query(DB_INITIALIZE_QUERY)
- .with_connection_initialize_query(CONNECTION_INITIALIZE_QUERY)
- .build()
- .await
- .log_err() {
- return Ok(connection)
+ create_dir_all(&main_db_dir).context("Could not create db directory")?;
+ let db_path = main_db_dir.join(Path::new("db.sqlite"));
+
+ // Optimistically open databases in parallel
+ if !DB_FILE_OPERATIONS.is_locked() {
+ // Try building a connection
+ if let Some(connection) = open_main_db(&db_path).await {
+ return Ok(connection)
+ };
}
+ // Take a lock in the failure case so that we move the db once per process instead
+ // of potentially multiple times from different threads. This shouldn't happen in the
+ // normal path
+ let _lock = DB_FILE_OPERATIONS.lock();
+ if let Some(connection) = open_main_db(&db_path).await {
+ return Ok(connection)
+ };
+
let backup_timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
- .expect(
- "System clock is set before the unix timestamp, Zed does not support this region of spacetime"
- )
+ .expect("System clock is set before the unix timestamp, Zed does not support this region of spacetime")
.as_millis();
// If failed, move 0-{channel} to {current unix timestamp}-{channel}
- let backup_db_dir = (*DB_DIR).join(Path::new(&format!(
- "{}{}",
+ let backup_db_dir = db_dir.join(Path::new(&format!(
+ "{}-{}",
backup_timestamp,
- *RELEASE_CHANNEL_NAME
+ release_channel.name(),
)));
- std::fs::rename(&db_dir, backup_db_dir)
+ std::fs::rename(&main_db_dir, &backup_db_dir)
.context("Failed clean up corrupted database, panicking.")?;
- // TODO: Set a constant with the failed timestamp and error so we can notify the user
-
+ // Set a static ref with the failed timestamp and error so we can notify the user
+ {
+ let mut guard = BACKUP_DB_PATH.write();
+ *guard = Some(backup_db_dir);
+ }
+
// Create a new 0-{channel}
- create_dir_all(&db_dir).context("Should be able to create the database directory")?;
- let db_path = db_dir.join(Path::new("db.sqlite"));
+ create_dir_all(&main_db_dir).context("Should be able to create the database directory")?;
+ let db_path = main_db_dir.join(Path::new("db.sqlite"));
// Try again
- ThreadSafeConnection::<M>::builder(db_path.to_string_lossy().as_ref(), true)
- .with_db_initialization_query(DB_INITIALIZE_QUERY)
- .with_connection_initialize_query(CONNECTION_INITIALIZE_QUERY)
- .build()
- .await
+ open_main_db(&db_path).await.context("Could not newly created db")
}).await.log_err();
- if let Some(connection) = connection {
+ if let Some(connection) = connection {
return connection;
}
- // TODO: Set another constant so that we can escalate the notification
+ // Set another static ref so that we can escalate the notification
+ ALL_FILE_DB_FAILED.store(true, Ordering::Release);
// If still failed, create an in memory db with a known name
open_fallback_db().await
}
+async fn open_main_db<M: Migrator>(db_path: &PathBuf) -> Option<ThreadSafeConnection<M>> {
+ println!("Opening main db");
+ ThreadSafeConnection::<M>::builder(db_path.to_string_lossy().as_ref(), true)
+ .with_db_initialization_query(DB_INITIALIZE_QUERY)
+ .with_connection_initialize_query(CONNECTION_INITIALIZE_QUERY)
+ .build()
+ .await
+ .log_err()
+}
+
async fn open_fallback_db<M: Migrator>() -> ThreadSafeConnection<M> {
+ println!("Opening fallback db");
ThreadSafeConnection::<M>::builder(FALLBACK_DB_NAME, false)
.with_db_initialization_query(DB_INITIALIZE_QUERY)
.with_connection_initialize_query(CONNECTION_INITIALIZE_QUERY)
@@ -135,17 +174,27 @@ pub async fn open_test_db<M: Migrator>(db_name: &str) -> ThreadSafeConnection<M>
/// Implements a basic DB wrapper for a given domain
#[macro_export]
-macro_rules! connection {
- ($id:ident: $t:ident<$d:ty>) => {
- pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection<$d>);
+macro_rules! define_connection {
+ (pub static ref $id:ident: $t:ident<()> = $migrations:expr;) => {
+ pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection<$t>);
impl ::std::ops::Deref for $t {
- type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection<$d>;
+ type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection<$t>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
+
+ impl $crate::sqlez::domain::Domain for $t {
+ fn name() -> &'static str {
+ stringify!($t)
+ }
+
+ fn migrations() -> &'static [&'static str] {
+ $migrations
+ }
+ }
#[cfg(any(test, feature = "test-support"))]
$crate::lazy_static::lazy_static! {
@@ -154,322 +203,124 @@ macro_rules! connection {
#[cfg(not(any(test, feature = "test-support")))]
$crate::lazy_static::lazy_static! {
- pub static ref $id: $t = $t($crate::smol::block_on($crate::open_db()));
- }
- };
-}
-
-#[macro_export]
-macro_rules! query {
- ($vis:vis fn $id:ident() -> Result<()> { $($sql:tt)+ }) => {
- $vis fn $id(&self) -> $crate::anyhow::Result<()> {
- use $crate::anyhow::Context;
-
- let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
-
- self.exec(sql_stmt)?().context(::std::format!(
- "Error in {}, exec failed to execute or parse for: {}",
- ::std::stringify!($id),
- sql_stmt,
- ))
+ pub static ref $id: $t = $t($crate::smol::block_on($crate::open_db(std::env::var("WIPE_DB").is_ok(), &$crate::DB_DIR, &$crate::RELEASE_CHANNEL)));
}
};
- ($vis:vis async fn $id:ident() -> Result<()> { $($sql:tt)+ }) => {
- $vis async fn $id(&self) -> $crate::anyhow::Result<()> {
- use $crate::anyhow::Context;
-
- self.write(|connection| {
- let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+ (pub static ref $id:ident: $t:ident<$($d:ty),+> = $migrations:expr;) => {
+ pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection<( $($d),+, $t )>);
- connection.exec(sql_stmt)?().context(::std::format!(
- "Error in {}, exec failed to execute or parse for: {}",
- ::std::stringify!($id),
- sql_stmt
- ))
- }).await
- }
- };
- ($vis:vis fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<()> { $($sql:tt)+ }) => {
- $vis fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<()> {
- use $crate::anyhow::Context;
-
- let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
-
- self.exec_bound::<($($arg_type),+)>(sql_stmt)?(($($arg),+))
- .context(::std::format!(
- "Error in {}, exec_bound failed to execute or parse for: {}",
- ::std::stringify!($id),
- sql_stmt
- ))
- }
- };
- ($vis:vis async fn $id:ident($arg:ident: $arg_type:ty) -> Result<()> { $($sql:tt)+ }) => {
- $vis async fn $id(&self, $arg: $arg_type) -> $crate::anyhow::Result<()> {
- use $crate::anyhow::Context;
-
- self.write(move |connection| {
- let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
-
- connection.exec_bound::<$arg_type>(sql_stmt)?($arg)
- .context(::std::format!(
- "Error in {}, exec_bound failed to execute or parse for: {}",
- ::std::stringify!($id),
- sql_stmt
- ))
- }).await
- }
- };
- ($vis:vis async fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<()> { $($sql:tt)+ }) => {
- $vis async fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<()> {
- use $crate::anyhow::Context;
-
- self.write(move |connection| {
- let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
-
- connection.exec_bound::<($($arg_type),+)>(sql_stmt)?(($($arg),+))
- .context(::std::format!(
- "Error in {}, exec_bound failed to execute or parse for: {}",
- ::std::stringify!($id),
- sql_stmt
- ))
- }).await
- }
- };
- ($vis:vis fn $id:ident() -> Result<Vec<$return_type:ty>> { $($sql:tt)+ }) => {
- $vis fn $id(&self) -> $crate::anyhow::Result<Vec<$return_type>> {
- use $crate::anyhow::Context;
-
- let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
-
- self.select::<$return_type>(sql_stmt)?(())
- .context(::std::format!(
- "Error in {}, select_row failed to execute or parse for: {}",
- ::std::stringify!($id),
- sql_stmt
- ))
- }
- };
- ($vis:vis async fn $id:ident() -> Result<Vec<$return_type:ty>> { $($sql:tt)+ }) => {
- pub async fn $id(&self) -> $crate::anyhow::Result<Vec<$return_type>> {
- use $crate::anyhow::Context;
-
- self.write(|connection| {
- let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
-
- connection.select::<$return_type>(sql_stmt)?(())
- .context(::std::format!(
- "Error in {}, select_row failed to execute or parse for: {}",
- ::std::stringify!($id),
- sql_stmt
- ))
- }).await
- }
- };
- ($vis:vis fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<Vec<$return_type:ty>> { $($sql:tt)+ }) => {
- $vis fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<Vec<$return_type>> {
- use $crate::anyhow::Context;
-
- let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
-
- self.select_bound::<($($arg_type),+), $return_type>(sql_stmt)?(($($arg),+))
- .context(::std::format!(
- "Error in {}, exec_bound failed to execute or parse for: {}",
- ::std::stringify!($id),
- sql_stmt
- ))
- }
- };
- ($vis:vis async fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<Vec<$return_type:ty>> { $($sql:tt)+ }) => {
- $vis async fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<Vec<$return_type>> {
- use $crate::anyhow::Context;
-
- self.write(|connection| {
- let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+ impl ::std::ops::Deref for $t {
+ type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection<($($d),+, $t)>;
- connection.select_bound::<($($arg_type),+), $return_type>(sql_stmt)?(($($arg),+))
- .context(::std::format!(
- "Error in {}, exec_bound failed to execute or parse for: {}",
- ::std::stringify!($id),
- sql_stmt
- ))
- }).await
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
}
- };
- ($vis:vis fn $id:ident() -> Result<Option<$return_type:ty>> { $($sql:tt)+ }) => {
- $vis fn $id(&self) -> $crate::anyhow::Result<Option<$return_type>> {
- use $crate::anyhow::Context;
-
- let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
-
- self.select_row::<$return_type>(sql_stmt)?()
- .context(::std::format!(
- "Error in {}, select_row failed to execute or parse for: {}",
- ::std::stringify!($id),
- sql_stmt
- ))
- }
- };
- ($vis:vis async fn $id:ident() -> Result<Option<$return_type:ty>> { $($sql:tt)+ }) => {
- $vis async fn $id(&self) -> $crate::anyhow::Result<Option<$return_type>> {
- use $crate::anyhow::Context;
-
- self.write(|connection| {
- let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
-
- connection.select_row::<$return_type>(sql_stmt)?()
- .context(::std::format!(
- "Error in {}, select_row failed to execute or parse for: {}",
- ::std::stringify!($id),
- sql_stmt
- ))
- }).await
+
+ impl $crate::sqlez::domain::Domain for $t {
+ fn name() -> &'static str {
+ stringify!($t)
+ }
+
+ fn migrations() -> &'static [&'static str] {
+ $migrations
+ }
}
- };
- ($vis:vis fn $id:ident($arg:ident: $arg_type:ty) -> Result<Option<$return_type:ty>> { $($sql:tt)+ }) => {
- $vis fn $id(&self, $arg: $arg_type) -> $crate::anyhow::Result<Option<$return_type>> {
- use $crate::anyhow::Context;
-
- let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
-
- self.select_row_bound::<$arg_type, $return_type>(sql_stmt)?($arg)
- .context(::std::format!(
- "Error in {}, select_row_bound failed to execute or parse for: {}",
- ::std::stringify!($id),
- sql_stmt
- ))
+ #[cfg(any(test, feature = "test-support"))]
+ $crate::lazy_static::lazy_static! {
+ pub static ref $id: $t = $t($crate::smol::block_on($crate::open_test_db(stringify!($id))));
}
- };
- ($vis:vis fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<Option<$return_type:ty>> { $($sql:tt)+ }) => {
- $vis fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<Option<$return_type>> {
- use $crate::anyhow::Context;
-
- let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
-
- self.select_row_bound::<($($arg_type),+), $return_type>(sql_stmt)?(($($arg),+))
- .context(::std::format!(
- "Error in {}, select_row_bound failed to execute or parse for: {}",
- ::std::stringify!($id),
- sql_stmt
- ))
-
- }
- };
- ($vis:vis async fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<Option<$return_type:ty>> { $($sql:tt)+ }) => {
- $vis async fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<Option<$return_type>> {
- use $crate::anyhow::Context;
-
-
- self.write(|connection| {
- let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
- connection.select_row_bound::<($($arg_type),+), $return_type>(indoc! { $sql })?(($($arg),+))
- .context(::std::format!(
- "Error in {}, select_row_bound failed to execute or parse for: {}",
- ::std::stringify!($id),
- sql_stmt
- ))
- }).await
+ #[cfg(not(any(test, feature = "test-support")))]
+ $crate::lazy_static::lazy_static! {
+ pub static ref $id: $t = $t($crate::smol::block_on($crate::open_db(std::env::var("WIPE_DB").is_ok(), &$crate::DB_DIR, &$crate::RELEASE_CHANNEL)));
}
};
- ($vis:vis fn $id:ident() -> Result<$return_type:ty> { $($sql:tt)+ }) => {
- $vis fn $id(&self) -> $crate::anyhow::Result<$return_type> {
- use $crate::anyhow::Context;
+}
- let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+#[cfg(test)]
+mod tests {
+ use std::thread;
- self.select_row::<$return_type>(indoc! { $sql })?()
- .context(::std::format!(
- "Error in {}, select_row_bound failed to execute or parse for: {}",
- ::std::stringify!($id),
- sql_stmt
- ))?
- .context(::std::format!(
- "Error in {}, select_row_bound expected single row result but found none for: {}",
- ::std::stringify!($id),
- sql_stmt
- ))
- }
- };
- ($vis:vis async fn $id:ident() -> Result<$return_type:ty> { $($sql:tt)+ }) => {
- $vis async fn $id(&self) -> $crate::anyhow::Result<$return_type> {
- use $crate::anyhow::Context;
+ use sqlez::domain::Domain;
+ use sqlez_macros::sql;
+ use tempdir::TempDir;
+ use util::channel::ReleaseChannel;
- self.write(|connection| {
- let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
-
- connection.select_row::<$return_type>(sql_stmt)?()
- .context(::std::format!(
- "Error in {}, select_row_bound failed to execute or parse for: {}",
- ::std::stringify!($id),
- sql_stmt
- ))?
- .context(::std::format!(
- "Error in {}, select_row_bound expected single row result but found none for: {}",
- ::std::stringify!($id),
- sql_stmt
- ))
- }).await
+ use crate::open_db;
+
+ enum TestDB {}
+
+ impl Domain for TestDB {
+ fn name() -> &'static str {
+ "db_tests"
}
- };
- ($vis:vis fn $id:ident($arg:ident: $arg_type:ty) -> Result<$return_type:ty> { $($sql:tt)+ }) => {
- pub fn $id(&self, $arg: $arg_type) -> $crate::anyhow::Result<$return_type> {
- use $crate::anyhow::Context;
-
- let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
- self.select_row_bound::<$arg_type, $return_type>(sql_stmt)?($arg)
- .context(::std::format!(
- "Error in {}, select_row_bound failed to execute or parse for: {}",
- ::std::stringify!($id),
- sql_stmt
- ))?
- .context(::std::format!(
- "Error in {}, select_row_bound expected single row result but found none for: {}",
- ::std::stringify!($id),
- sql_stmt
- ))
+ fn migrations() -> &'static [&'static str] {
+ &[sql!(
+ CREATE TABLE test(value);
+ )]
}
- };
- ($vis:vis fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<$return_type:ty> { $($sql:tt)+ }) => {
- $vis fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<$return_type> {
- use $crate::anyhow::Context;
-
- let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
-
- self.select_row_bound::<($($arg_type),+), $return_type>(sql_stmt)?(($($arg),+))
- .context(::std::format!(
- "Error in {}, select_row_bound failed to execute or parse for: {}",
- ::std::stringify!($id),
- sql_stmt
- ))?
- .context(::std::format!(
- "Error in {}, select_row_bound expected single row result but found none for: {}",
- ::std::stringify!($id),
- sql_stmt
- ))
- }
- };
- ($vis:vis fn async $id:ident($($arg:ident: $arg_type:ty),+) -> Result<$return_type:ty> { $($sql:tt)+ }) => {
- $vis async fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<$return_type> {
- use $crate::anyhow::Context;
-
-
- self.write(|connection| {
- let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+ }
+
+ // Test that wipe_db exists and works and gives a new db
+ #[test]
+ fn test_wipe_db() {
+ env_logger::try_init().ok();
+
+ smol::block_on(async {
+ let tempdir = TempDir::new("DbTests").unwrap();
+
+ let test_db = open_db::<TestDB>(false, tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
+ test_db.write(|connection|
+ connection.exec(sql!(
+ INSERT INTO test(value) VALUES (10)
+ )).unwrap()().unwrap()
+ ).await;
+ drop(test_db);
+
+ let mut guards = vec![];
+ for _ in 0..5 {
+ let path = tempdir.path().to_path_buf();
+ let guard = thread::spawn(move || smol::block_on(async {
+ let test_db = open_db::<TestDB>(true, &path, &ReleaseChannel::Dev).await;
+
+ assert!(test_db.select_row::<()>(sql!(SELECT value FROM test)).unwrap()().unwrap().is_none())
+ }));
+
+ guards.push(guard);
+ }
+
+ for guard in guards {
+ guard.join().unwrap();
+ }
+ })
+ }
- connection.select_row_bound::<($($arg_type),+), $return_type>(sql_stmt)?(($($arg),+))
- .context(::std::format!(
- "Error in {}, select_row_bound failed to execute or parse for: {}",
- ::std::stringify!($id),
- sql_stmt
- ))?
- .context(::std::format!(
- "Error in {}, select_row_bound expected single row result but found none for: {}",
- ::std::stringify!($id),
- sql_stmt
- ))
- }).await
- }
- };
+ // Test a file system failure (like in create_dir_all())
+ #[test]
+ fn test_file_system_failure() {
+
+ }
+
+ // Test happy path where everything exists and opens
+ #[test]
+ fn test_open_db() {
+
+ }
+
+ // Test bad migration panics
+ #[test]
+ fn test_bad_migration_panics() {
+
+ }
+
+ /// Test that DB exists but corrupted (causing recreate)
+ #[test]
+ fn test_db_corruption() {
+
+
+ // open_db(db_dir, release_channel)
+ }
}
@@ -1,26 +1,15 @@
-use sqlez::domain::Domain;
use sqlez_macros::sql;
-use crate::{connection, query};
+use crate::{define_connection, query};
-connection!(KEY_VALUE_STORE: KeyValueStore<KeyValueStore>);
-
-impl Domain for KeyValueStore {
- fn name() -> &'static str {
- "kvp"
- }
-
- fn migrations() -> &'static [&'static str] {
- // Legacy migrations using rusqlite may have already created kv_store during alpha,
- // migrations must be infallible so this must have 'IF NOT EXISTS'
- &[sql!(
- CREATE TABLE IF NOT EXISTS kv_store(
- key TEXT PRIMARY KEY,
- value TEXT NOT NULL
- ) STRICT;
- )]
- }
-}
+define_connection!(pub static ref KEY_VALUE_STORE: KeyValueStore<()> =
+ &[sql!(
+ CREATE TABLE IF NOT EXISTS kv_store(
+ key TEXT PRIMARY KEY,
+ value TEXT NOT NULL
+ ) STRICT;
+ )];
+);
impl KeyValueStore {
query! {
@@ -0,0 +1,314 @@
+#[macro_export]
+macro_rules! query {
+ ($vis:vis fn $id:ident() -> Result<()> { $($sql:tt)+ }) => {
+ $vis fn $id(&self) -> $crate::anyhow::Result<()> {
+ use $crate::anyhow::Context;
+
+ let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+ self.exec(sql_stmt)?().context(::std::format!(
+ "Error in {}, exec failed to execute or parse for: {}",
+ ::std::stringify!($id),
+ sql_stmt,
+ ))
+ }
+ };
+ ($vis:vis async fn $id:ident() -> Result<()> { $($sql:tt)+ }) => {
+ $vis async fn $id(&self) -> $crate::anyhow::Result<()> {
+ use $crate::anyhow::Context;
+
+ self.write(|connection| {
+ let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+ connection.exec(sql_stmt)?().context(::std::format!(
+ "Error in {}, exec failed to execute or parse for: {}",
+ ::std::stringify!($id),
+ sql_stmt
+ ))
+ }).await
+ }
+ };
+ ($vis:vis fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<()> { $($sql:tt)+ }) => {
+ $vis fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<()> {
+ use $crate::anyhow::Context;
+
+ let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+ self.exec_bound::<($($arg_type),+)>(sql_stmt)?(($($arg),+))
+ .context(::std::format!(
+ "Error in {}, exec_bound failed to execute or parse for: {}",
+ ::std::stringify!($id),
+ sql_stmt
+ ))
+ }
+ };
+ ($vis:vis async fn $id:ident($arg:ident: $arg_type:ty) -> Result<()> { $($sql:tt)+ }) => {
+ $vis async fn $id(&self, $arg: $arg_type) -> $crate::anyhow::Result<()> {
+ use $crate::anyhow::Context;
+
+ self.write(move |connection| {
+ let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+ connection.exec_bound::<$arg_type>(sql_stmt)?($arg)
+ .context(::std::format!(
+ "Error in {}, exec_bound failed to execute or parse for: {}",
+ ::std::stringify!($id),
+ sql_stmt
+ ))
+ }).await
+ }
+ };
+ ($vis:vis async fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<()> { $($sql:tt)+ }) => {
+ $vis async fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<()> {
+ use $crate::anyhow::Context;
+
+ self.write(move |connection| {
+ let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+ connection.exec_bound::<($($arg_type),+)>(sql_stmt)?(($($arg),+))
+ .context(::std::format!(
+ "Error in {}, exec_bound failed to execute or parse for: {}",
+ ::std::stringify!($id),
+ sql_stmt
+ ))
+ }).await
+ }
+ };
+ ($vis:vis fn $id:ident() -> Result<Vec<$return_type:ty>> { $($sql:tt)+ }) => {
+ $vis fn $id(&self) -> $crate::anyhow::Result<Vec<$return_type>> {
+ use $crate::anyhow::Context;
+
+ let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+ self.select::<$return_type>(sql_stmt)?(())
+ .context(::std::format!(
+ "Error in {}, select_row failed to execute or parse for: {}",
+ ::std::stringify!($id),
+ sql_stmt
+ ))
+ }
+ };
+ ($vis:vis async fn $id:ident() -> Result<Vec<$return_type:ty>> { $($sql:tt)+ }) => {
+ pub async fn $id(&self) -> $crate::anyhow::Result<Vec<$return_type>> {
+ use $crate::anyhow::Context;
+
+ self.write(|connection| {
+ let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+ connection.select::<$return_type>(sql_stmt)?(())
+ .context(::std::format!(
+ "Error in {}, select_row failed to execute or parse for: {}",
+ ::std::stringify!($id),
+ sql_stmt
+ ))
+ }).await
+ }
+ };
+ ($vis:vis fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<Vec<$return_type:ty>> { $($sql:tt)+ }) => {
+ $vis fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<Vec<$return_type>> {
+ use $crate::anyhow::Context;
+
+ let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+ self.select_bound::<($($arg_type),+), $return_type>(sql_stmt)?(($($arg),+))
+ .context(::std::format!(
+ "Error in {}, exec_bound failed to execute or parse for: {}",
+ ::std::stringify!($id),
+ sql_stmt
+ ))
+ }
+ };
+ ($vis:vis async fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<Vec<$return_type:ty>> { $($sql:tt)+ }) => {
+ $vis async fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<Vec<$return_type>> {
+ use $crate::anyhow::Context;
+
+ self.write(|connection| {
+ let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+ connection.select_bound::<($($arg_type),+), $return_type>(sql_stmt)?(($($arg),+))
+ .context(::std::format!(
+ "Error in {}, exec_bound failed to execute or parse for: {}",
+ ::std::stringify!($id),
+ sql_stmt
+ ))
+ }).await
+ }
+ };
+ ($vis:vis fn $id:ident() -> Result<Option<$return_type:ty>> { $($sql:tt)+ }) => {
+ $vis fn $id(&self) -> $crate::anyhow::Result<Option<$return_type>> {
+ use $crate::anyhow::Context;
+
+ let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+ self.select_row::<$return_type>(sql_stmt)?()
+ .context(::std::format!(
+ "Error in {}, select_row failed to execute or parse for: {}",
+ ::std::stringify!($id),
+ sql_stmt
+ ))
+ }
+ };
+ ($vis:vis async fn $id:ident() -> Result<Option<$return_type:ty>> { $($sql:tt)+ }) => {
+ $vis async fn $id(&self) -> $crate::anyhow::Result<Option<$return_type>> {
+ use $crate::anyhow::Context;
+
+ self.write(|connection| {
+ let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+ connection.select_row::<$return_type>(sql_stmt)?()
+ .context(::std::format!(
+ "Error in {}, select_row failed to execute or parse for: {}",
+ ::std::stringify!($id),
+ sql_stmt
+ ))
+ }).await
+ }
+ };
+ ($vis:vis fn $id:ident($arg:ident: $arg_type:ty) -> Result<Option<$return_type:ty>> { $($sql:tt)+ }) => {
+ $vis fn $id(&self, $arg: $arg_type) -> $crate::anyhow::Result<Option<$return_type>> {
+ use $crate::anyhow::Context;
+
+ let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+ self.select_row_bound::<$arg_type, $return_type>(sql_stmt)?($arg)
+ .context(::std::format!(
+ "Error in {}, select_row_bound failed to execute or parse for: {}",
+ ::std::stringify!($id),
+ sql_stmt
+ ))
+
+ }
+ };
+ ($vis:vis fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<Option<$return_type:ty>> { $($sql:tt)+ }) => {
+ $vis fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<Option<$return_type>> {
+ use $crate::anyhow::Context;
+
+ let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+ self.select_row_bound::<($($arg_type),+), $return_type>(sql_stmt)?(($($arg),+))
+ .context(::std::format!(
+ "Error in {}, select_row_bound failed to execute or parse for: {}",
+ ::std::stringify!($id),
+ sql_stmt
+ ))
+
+ }
+ };
+ ($vis:vis async fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<Option<$return_type:ty>> { $($sql:tt)+ }) => {
+ $vis async fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<Option<$return_type>> {
+ use $crate::anyhow::Context;
+
+
+ self.write(|connection| {
+ let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+ connection.select_row_bound::<($($arg_type),+), $return_type>(indoc! { $sql })?(($($arg),+))
+ .context(::std::format!(
+ "Error in {}, select_row_bound failed to execute or parse for: {}",
+ ::std::stringify!($id),
+ sql_stmt
+ ))
+ }).await
+ }
+ };
+ ($vis:vis fn $id:ident() -> Result<$return_type:ty> { $($sql:tt)+ }) => {
+ $vis fn $id(&self) -> $crate::anyhow::Result<$return_type> {
+ use $crate::anyhow::Context;
+
+ let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+ self.select_row::<$return_type>(indoc! { $sql })?()
+ .context(::std::format!(
+ "Error in {}, select_row_bound failed to execute or parse for: {}",
+ ::std::stringify!($id),
+ sql_stmt
+ ))?
+ .context(::std::format!(
+ "Error in {}, select_row_bound expected single row result but found none for: {}",
+ ::std::stringify!($id),
+ sql_stmt
+ ))
+ }
+ };
+ ($vis:vis async fn $id:ident() -> Result<$return_type:ty> { $($sql:tt)+ }) => {
+ $vis async fn $id(&self) -> $crate::anyhow::Result<$return_type> {
+ use $crate::anyhow::Context;
+
+ self.write(|connection| {
+ let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+ connection.select_row::<$return_type>(sql_stmt)?()
+ .context(::std::format!(
+ "Error in {}, select_row_bound failed to execute or parse for: {}",
+ ::std::stringify!($id),
+ sql_stmt
+ ))?
+ .context(::std::format!(
+ "Error in {}, select_row_bound expected single row result but found none for: {}",
+ ::std::stringify!($id),
+ sql_stmt
+ ))
+ }).await
+ }
+ };
+ ($vis:vis fn $id:ident($arg:ident: $arg_type:ty) -> Result<$return_type:ty> { $($sql:tt)+ }) => {
+ pub fn $id(&self, $arg: $arg_type) -> $crate::anyhow::Result<$return_type> {
+ use $crate::anyhow::Context;
+
+ let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+ self.select_row_bound::<$arg_type, $return_type>(sql_stmt)?($arg)
+ .context(::std::format!(
+ "Error in {}, select_row_bound failed to execute or parse for: {}",
+ ::std::stringify!($id),
+ sql_stmt
+ ))?
+ .context(::std::format!(
+ "Error in {}, select_row_bound expected single row result but found none for: {}",
+ ::std::stringify!($id),
+ sql_stmt
+ ))
+ }
+ };
+ ($vis:vis fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<$return_type:ty> { $($sql:tt)+ }) => {
+ $vis fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<$return_type> {
+ use $crate::anyhow::Context;
+
+ let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+ self.select_row_bound::<($($arg_type),+), $return_type>(sql_stmt)?(($($arg),+))
+ .context(::std::format!(
+ "Error in {}, select_row_bound failed to execute or parse for: {}",
+ ::std::stringify!($id),
+ sql_stmt
+ ))?
+ .context(::std::format!(
+ "Error in {}, select_row_bound expected single row result but found none for: {}",
+ ::std::stringify!($id),
+ sql_stmt
+ ))
+ }
+ };
+ ($vis:vis fn async $id:ident($($arg:ident: $arg_type:ty),+) -> Result<$return_type:ty> { $($sql:tt)+ }) => {
+ $vis async fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<$return_type> {
+ use $crate::anyhow::Context;
+
+
+ self.write(|connection| {
+ let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
+
+ connection.select_row_bound::<($($arg_type),+), $return_type>(sql_stmt)?(($($arg),+))
+ .context(::std::format!(
+ "Error in {}, select_row_bound failed to execute or parse for: {}",
+ ::std::stringify!($id),
+ sql_stmt
+ ))?
+ .context(::std::format!(
+ "Error in {}, select_row_bound expected single row result but found none for: {}",
+ ::std::stringify!($id),
+ sql_stmt
+ ))
+ }).await
+ }
+ };
+}
@@ -1,19 +1,11 @@
use std::path::PathBuf;
-use crate::Editor;
use db::sqlez_macros::sql;
-use db::{connection, query};
-use sqlez::domain::Domain;
-use workspace::{ItemId, Workspace, WorkspaceId};
+use db::{define_connection, query};
+use workspace::{ItemId, WorkspaceDb, WorkspaceId};
-connection!(DB: EditorDb<(Workspace, Editor)>);
-
-impl Domain for Editor {
- fn name() -> &'static str {
- "editor"
- }
-
- fn migrations() -> &'static [&'static str] {
+define_connection!(
+ pub static ref DB: EditorDb<WorkspaceDb> =
&[sql! (
CREATE TABLE editors(
item_id INTEGER NOT NULL,
@@ -21,12 +13,11 @@ impl Domain for Editor {
path BLOB NOT NULL,
PRIMARY KEY(item_id, workspace_id),
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
- ON DELETE CASCADE
- ON UPDATE CASCADE
- ) STRICT;
- )]
- }
-}
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+ ) STRICT;
+ )];
+);
impl EditorDb {
query! {
@@ -137,13 +137,6 @@ impl Column for usize {
}
}
-impl Bind for () {
- fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
- statement.bind_null(start_index)?;
- Ok(start_index + 1)
- }
-}
-
impl Bind for &str {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
statement.bind_text(start_index, self)?;
@@ -179,78 +172,6 @@ impl Column for String {
}
}
-impl<T1: Bind, T2: Bind> Bind for (T1, T2) {
- fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
- let next_index = self.0.bind(statement, start_index)?;
- self.1.bind(statement, next_index)
- }
-}
-
-impl<T1: Column, T2: Column> Column for (T1, T2) {
- fn column<'a>(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
- let (first, next_index) = T1::column(statement, start_index)?;
- let (second, next_index) = T2::column(statement, next_index)?;
- Ok(((first, second), next_index))
- }
-}
-
-impl<T1: Bind, T2: Bind, T3: Bind> Bind for (T1, T2, T3) {
- fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
- let next_index = self.0.bind(statement, start_index)?;
- let next_index = self.1.bind(statement, next_index)?;
- self.2.bind(statement, next_index)
- }
-}
-
-impl<T1: Column, T2: Column, T3: Column> Column for (T1, T2, T3) {
- fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
- let (first, next_index) = T1::column(statement, start_index)?;
- let (second, next_index) = T2::column(statement, next_index)?;
- let (third, next_index) = T3::column(statement, next_index)?;
- Ok(((first, second, third), next_index))
- }
-}
-
-impl<T1: Bind, T2: Bind, T3: Bind, T4: Bind> Bind for (T1, T2, T3, T4) {
- fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
- let next_index = self.0.bind(statement, start_index)?;
- let next_index = self.1.bind(statement, next_index)?;
- let next_index = self.2.bind(statement, next_index)?;
- self.3.bind(statement, next_index)
- }
-}
-
-impl<T1: Column, T2: Column, T3: Column, T4: Column> Column for (T1, T2, T3, T4) {
- fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
- let (first, next_index) = T1::column(statement, start_index)?;
- let (second, next_index) = T2::column(statement, next_index)?;
- let (third, next_index) = T3::column(statement, next_index)?;
- let (fourth, next_index) = T4::column(statement, next_index)?;
- Ok(((first, second, third, fourth), next_index))
- }
-}
-
-impl<T1: Bind, T2: Bind, T3: Bind, T4: Bind, T5: Bind> Bind for (T1, T2, T3, T4, T5) {
- fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
- let next_index = self.0.bind(statement, start_index)?;
- let next_index = self.1.bind(statement, next_index)?;
- let next_index = self.2.bind(statement, next_index)?;
- let next_index = self.3.bind(statement, next_index)?;
- self.4.bind(statement, next_index)
- }
-}
-
-impl<T1: Column, T2: Column, T3: Column, T4: Column, T5: Column> Column for (T1, T2, T3, T4, T5) {
- fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
- let (first, next_index) = T1::column(statement, start_index)?;
- let (second, next_index) = T2::column(statement, next_index)?;
- let (third, next_index) = T3::column(statement, next_index)?;
- let (fourth, next_index) = T4::column(statement, next_index)?;
- let (fifth, next_index) = T5::column(statement, next_index)?;
- Ok(((first, second, third, fourth, fifth), next_index))
- }
-}
-
impl<T: Bind> Bind for Option<T> {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
if let Some(this) = self {
@@ -344,3 +265,88 @@ impl Column for PathBuf {
))
}
}
+
+/// 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))
+ }
+}
+
+impl<T1: Bind, T2: Bind> Bind for (T1, T2) {
+ fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
+ let next_index = self.0.bind(statement, start_index)?;
+ self.1.bind(statement, next_index)
+ }
+}
+
+impl<T1: Column, T2: Column> Column for (T1, T2) {
+ fn column<'a>(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
+ let (first, next_index) = T1::column(statement, start_index)?;
+ let (second, next_index) = T2::column(statement, next_index)?;
+ Ok(((first, second), next_index))
+ }
+}
+
+impl<T1: Bind, T2: Bind, T3: Bind> Bind for (T1, T2, T3) {
+ fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
+ let next_index = self.0.bind(statement, start_index)?;
+ let next_index = self.1.bind(statement, next_index)?;
+ self.2.bind(statement, next_index)
+ }
+}
+
+impl<T1: Column, T2: Column, T3: Column> Column for (T1, T2, T3) {
+ fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
+ let (first, next_index) = T1::column(statement, start_index)?;
+ let (second, next_index) = T2::column(statement, next_index)?;
+ let (third, next_index) = T3::column(statement, next_index)?;
+ Ok(((first, second, third), next_index))
+ }
+}
+
+impl<T1: Bind, T2: Bind, T3: Bind, T4: Bind> Bind for (T1, T2, T3, T4) {
+ fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
+ let next_index = self.0.bind(statement, start_index)?;
+ let next_index = self.1.bind(statement, next_index)?;
+ let next_index = self.2.bind(statement, next_index)?;
+ self.3.bind(statement, next_index)
+ }
+}
+
+impl<T1: Column, T2: Column, T3: Column, T4: Column> Column for (T1, T2, T3, T4) {
+ fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
+ let (first, next_index) = T1::column(statement, start_index)?;
+ let (second, next_index) = T2::column(statement, next_index)?;
+ let (third, next_index) = T3::column(statement, next_index)?;
+ let (fourth, next_index) = T4::column(statement, next_index)?;
+ Ok(((first, second, third, fourth), next_index))
+ }
+}
+
+impl<T1: Bind, T2: Bind, T3: Bind, T4: Bind, T5: Bind> Bind for (T1, T2, T3, T4, T5) {
+ fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
+ let next_index = self.0.bind(statement, start_index)?;
+ let next_index = self.1.bind(statement, next_index)?;
+ let next_index = self.2.bind(statement, next_index)?;
+ let next_index = self.3.bind(statement, next_index)?;
+ self.4.bind(statement, next_index)
+ }
+}
+
+impl<T1: Column, T2: Column, T3: Column, T4: Column, T5: Column> Column for (T1, T2, T3, T4, T5) {
+ fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
+ let (first, next_index) = T1::column(statement, start_index)?;
+ let (second, next_index) = T2::column(statement, next_index)?;
+ let (third, next_index) = T3::column(statement, next_index)?;
+ let (fourth, next_index) = T4::column(statement, next_index)?;
+ let (fifth, next_index) = T5::column(statement, next_index)?;
+ Ok(((first, second, third, fourth, fifth), next_index))
+ }
+}
@@ -1,4 +1,5 @@
use std::{
+ cell::RefCell,
ffi::{CStr, CString},
marker::PhantomData,
path::Path,
@@ -11,7 +12,7 @@ use libsqlite3_sys::*;
pub struct Connection {
pub(crate) sqlite3: *mut sqlite3,
persistent: bool,
- pub(crate) write: bool,
+ pub(crate) write: RefCell<bool>,
_sqlite: PhantomData<sqlite3>,
}
unsafe impl Send for Connection {}
@@ -21,7 +22,7 @@ impl Connection {
let mut connection = Self {
sqlite3: 0 as *mut _,
persistent,
- write: true,
+ write: RefCell::new(true),
_sqlite: PhantomData,
};
@@ -64,7 +65,7 @@ impl Connection {
}
pub fn can_write(&self) -> bool {
- self.write
+ *self.write.borrow()
}
pub fn backup_main(&self, destination: &Connection) -> Result<()> {
@@ -152,6 +153,13 @@ impl Connection {
))
}
}
+
+ 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
+ }
}
impl Drop for Connection {
@@ -1,11 +1,11 @@
use crate::connection::Connection;
-pub trait Domain {
+pub trait Domain: 'static {
fn name() -> &'static str;
fn migrations() -> &'static [&'static str];
}
-pub trait Migrator {
+pub trait Migrator: 'static {
fn migrate(connection: &Connection) -> anyhow::Result<()>;
}
@@ -12,6 +12,7 @@ use crate::connection::Connection;
impl Connection {
pub fn migrate(&self, domain: &'static str, migrations: &[&'static str]) -> Result<()> {
self.with_savepoint("migrating", || {
+ println!("Processing domain");
// Setup the migrations table unconditionally
self.exec(indoc! {"
CREATE TABLE IF NOT EXISTS migrations (
@@ -43,11 +44,13 @@ impl Connection {
{}", domain, index, completed_migration, migration}));
} else {
// Migration already run. Continue
+ println!("Migration already run");
continue;
}
}
self.exec(migration)?()?;
+ println!("Ran migration");
store_completed_migration((domain, index, *migration))?;
}
@@ -5,17 +5,13 @@ use parking_lot::{Mutex, RwLock};
use std::{collections::HashMap, marker::PhantomData, ops::Deref, sync::Arc, thread};
use thread_local::ThreadLocal;
-use crate::{
- connection::Connection,
- domain::{Domain, Migrator},
- util::UnboundedSyncSender,
-};
+use crate::{connection::Connection, domain::Migrator, util::UnboundedSyncSender};
const MIGRATION_RETRIES: usize = 10;
-type QueuedWrite = Box<dyn 'static + Send + FnOnce(&Connection)>;
+type QueuedWrite = Box<dyn 'static + Send + FnOnce()>;
type WriteQueueConstructor =
- Box<dyn 'static + Send + FnMut(Connection) -> Box<dyn 'static + Send + Sync + Fn(QueuedWrite)>>;
+ Box<dyn 'static + Send + FnMut() -> Box<dyn 'static + Send + Sync + Fn(QueuedWrite)>>;
lazy_static! {
/// 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
@@ -28,18 +24,18 @@ lazy_static! {
/// 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
-pub struct ThreadSafeConnection<M: Migrator = ()> {
+pub struct ThreadSafeConnection<M: Migrator + 'static = ()> {
uri: Arc<str>,
persistent: bool,
connection_initialize_query: Option<&'static str>,
connections: Arc<ThreadLocal<Connection>>,
- _migrator: PhantomData<M>,
+ _migrator: PhantomData<*mut M>,
}
-unsafe impl<T: Migrator> Send for ThreadSafeConnection<T> {}
-unsafe impl<T: Migrator> Sync for ThreadSafeConnection<T> {}
+unsafe impl<M: Migrator> Send for ThreadSafeConnection<M> {}
+unsafe impl<M: Migrator> Sync for ThreadSafeConnection<M> {}
-pub struct ThreadSafeConnectionBuilder<M: Migrator = ()> {
+pub struct ThreadSafeConnectionBuilder<M: Migrator + 'static = ()> {
db_initialize_query: Option<&'static str>,
write_queue_constructor: Option<WriteQueueConstructor>,
connection: ThreadSafeConnection<M>,
@@ -54,6 +50,13 @@ impl<M: Migrator> ThreadSafeConnectionBuilder<M> {
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
@@ -66,13 +69,6 @@ impl<M: Migrator> ThreadSafeConnectionBuilder<M> {
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
- }
-
pub async fn build(self) -> anyhow::Result<ThreadSafeConnection<M>> {
self.connection
.initialize_queues(self.write_queue_constructor);
@@ -100,6 +96,7 @@ impl<M: Migrator> ThreadSafeConnectionBuilder<M> {
.with_savepoint("thread_safe_multi_migration", || M::migrate(connection));
if migration_result.is_ok() {
+ println!("Migration succeded");
break;
}
}
@@ -113,38 +110,17 @@ impl<M: Migrator> ThreadSafeConnectionBuilder<M> {
}
impl<M: Migrator> ThreadSafeConnection<M> {
- fn initialize_queues(&self, write_queue_constructor: Option<WriteQueueConstructor>) {
+ 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_connection = self.create_connection();
- // Enable writes for this connection
- write_connection.write = true;
- if let Some(mut write_queue_constructor) = write_queue_constructor {
- let write_channel = write_queue_constructor(write_connection);
- queues.insert(self.uri.clone(), write_channel);
- } else {
- use std::sync::mpsc::channel;
-
- let (sender, reciever) = channel::<QueuedWrite>();
- thread::spawn(move || {
- while let Ok(write) = reciever.recv() {
- write(&write_connection)
- }
- });
-
- let sender = UnboundedSyncSender::new(sender);
- queues.insert(
- self.uri.clone(),
- Box::new(move |queued_write| {
- sender
- .send(queued_write)
- .expect("Could not send write action to backgorund thread");
- }),
- );
- }
+ let mut write_queue_constructor =
+ write_queue_constructor.unwrap_or(background_thread_queue());
+ queues.insert(self.uri.clone(), write_queue_constructor());
+ return true;
}
}
+ return false;
}
pub fn builder(uri: &str, persistent: bool) -> ThreadSafeConnectionBuilder<M> {
@@ -163,20 +139,21 @@ impl<M: Migrator> ThreadSafeConnection<M> {
/// Opens a new db connection with the initialized file path. This is internal and only
/// called from the deref function.
- fn open_file(&self) -> Connection {
- Connection::open_file(self.uri.as_ref())
+ 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(&self) -> Connection {
- Connection::open_memory(Some(self.uri.as_ref()))
+ 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)
@@ -185,24 +162,32 @@ impl<M: Migrator> ThreadSafeConnection<M> {
// Create a one shot channel for the result of the queued write
// so we can await on the result
let (sender, reciever) = oneshot::channel();
- write_channel(Box::new(move |connection| {
- sender.send(callback(connection)).ok();
+
+ 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();
}));
reciever.map(|response| response.expect("Background writer thread unexpectedly closed"))
}
- pub(crate) fn create_connection(&self) -> Connection {
- let mut connection = if self.persistent {
- self.open_file()
+ 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()
+ Self::open_shared_memory(uri)
};
// Disallow writes on the connection. The only writes allowed for thread safe connections
// are from the background thread that can serialize them.
- connection.write = false;
+ *connection.write.get_mut() = false;
- if let Some(initialize_query) = self.connection_initialize_query {
+ if let Some(initialize_query) = connection_initialize_query {
connection.exec(initialize_query).expect(&format!(
"Initialize query failed to execute: {}",
initialize_query
@@ -236,7 +221,7 @@ impl ThreadSafeConnection<()> {
}
}
-impl<D: Domain> Clone for ThreadSafeConnection<D> {
+impl<M: Migrator> Clone for ThreadSafeConnection<M> {
fn clone(&self) -> Self {
Self {
uri: self.uri.clone(),
@@ -252,16 +237,41 @@ impl<M: Migrator> Deref for ThreadSafeConnection<M> {
type Target = Connection;
fn deref(&self) -> &Self::Target {
- self.connections.get_or(|| self.create_connection())
+ 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, reciever) = channel::<QueuedWrite>();
+
+ thread::spawn(move || {
+ while let Ok(write) = reciever.recv() {
+ write()
+ }
+ });
+
+ 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(|connection| {
- let connection = Mutex::new(connection);
+ Box::new(|| {
+ let mutex = Mutex::new(());
Box::new(move |queued_write| {
- let connection = connection.lock();
- queued_write(&connection)
+ eprintln!("Write started");
+ let _ = mutex.lock();
+ queued_write();
+ eprintln!("Write finished");
})
})
}
@@ -269,7 +279,8 @@ pub fn locking_queue() -> WriteQueueConstructor {
#[cfg(test)]
mod test {
use indoc::indoc;
- use std::ops::Deref;
+ use lazy_static::__Deref;
+
use std::thread;
use crate::{domain::Domain, thread_safe_connection::ThreadSafeConnection};
@@ -1,19 +1,11 @@
use std::path::PathBuf;
-use db::{connection, query, sqlez::domain::Domain, sqlez_macros::sql};
+use db::{define_connection, query, sqlez_macros::sql};
-use workspace::{ItemId, Workspace, WorkspaceId};
+use workspace::{ItemId, WorkspaceDb, WorkspaceId};
-use crate::Terminal;
-
-connection!(TERMINAL_CONNECTION: TerminalDb<(Workspace, Terminal)>);
-
-impl Domain for Terminal {
- fn name() -> &'static str {
- "terminal"
- }
-
- fn migrations() -> &'static [&'static str] {
+define_connection! {
+ pub static ref TERMINAL_CONNECTION: TerminalDb<WorkspaceDb> =
&[sql!(
CREATE TABLE terminals (
workspace_id INTEGER,
@@ -23,8 +15,7 @@ impl Domain for Terminal {
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
ON DELETE CASCADE
) STRICT;
- )]
- }
+ )];
}
impl TerminalDb {
@@ -5,30 +5,21 @@ pub mod model;
use std::path::Path;
use anyhow::{anyhow, bail, Context, Result};
-use db::{connection, query, sqlez::connection::Connection, sqlez_macros::sql};
+use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql};
use gpui::Axis;
-use db::sqlez::domain::Domain;
use util::{iife, unzip_option, ResultExt};
use crate::dock::DockPosition;
use crate::WorkspaceId;
-use super::Workspace;
-
use model::{
GroupId, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
WorkspaceLocation,
};
-connection!(DB: WorkspaceDb<Workspace>);
-
-impl Domain for Workspace {
- fn name() -> &'static str {
- "workspace"
- }
-
- fn migrations() -> &'static [&'static str] {
+define_connection! {
+ pub static ref DB: WorkspaceDb<()> =
&[sql!(
CREATE TABLE workspaces(
workspace_id INTEGER PRIMARY KEY,
@@ -40,7 +31,7 @@ impl Domain for Workspace {
timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
FOREIGN KEY(dock_pane) REFERENCES panes(pane_id)
) STRICT;
-
+
CREATE TABLE pane_groups(
group_id INTEGER PRIMARY KEY,
workspace_id INTEGER NOT NULL,
@@ -48,29 +39,29 @@ impl Domain for Workspace {
position INTEGER, // NULL indicates that this is a root node
axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal'
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
- ON DELETE CASCADE
- ON UPDATE CASCADE,
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
) STRICT;
-
+
CREATE TABLE panes(
pane_id INTEGER PRIMARY KEY,
workspace_id INTEGER NOT NULL,
active INTEGER NOT NULL, // Boolean
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
- ON DELETE CASCADE
- ON UPDATE CASCADE
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
) STRICT;
-
+
CREATE TABLE center_panes(
pane_id INTEGER PRIMARY KEY,
parent_group_id INTEGER, // NULL means that this is a root pane
position INTEGER, // NULL means that this is a root pane
FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
- ON DELETE CASCADE,
+ ON DELETE CASCADE,
FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
) STRICT;
-
+
CREATE TABLE items(
item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique
workspace_id INTEGER NOT NULL,
@@ -79,14 +70,13 @@ impl Domain for Workspace {
position INTEGER NOT NULL,
active INTEGER NOT NULL,
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
- ON DELETE CASCADE
- ON UPDATE CASCADE,
+ ON DELETE CASCADE
+ ON UPDATE CASCADE,
FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
- ON DELETE CASCADE,
+ ON DELETE CASCADE,
PRIMARY KEY(item_id, workspace_id)
) STRICT;
- )]
- }
+ )];
}
impl WorkspaceDb {
@@ -149,7 +139,7 @@ impl WorkspaceDb {
UPDATE workspaces SET dock_pane = NULL WHERE workspace_id = ?1;
DELETE FROM pane_groups WHERE workspace_id = ?1;
DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id)
- .context("Clearing old panes")?;
+ .expect("Clearing old panes");
conn.exec_bound(sql!(
DELETE FROM workspaces WHERE workspace_location = ? AND workspace_id != ?
@@ -44,8 +44,11 @@ use language::LanguageRegistry;
use log::{error, warn};
pub use pane::*;
pub use pane_group::*;
-pub use persistence::model::{ItemId, WorkspaceLocation};
use persistence::{model::SerializedItem, DB};
+pub use persistence::{
+ model::{ItemId, WorkspaceLocation},
+ WorkspaceDb,
+};
use postage::prelude::Stream;
use project::{Project, ProjectEntryId, ProjectPath, ProjectStore, Worktree, WorktreeId};
use serde::Deserialize;