-
Notifications
You must be signed in to change notification settings - Fork 548
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Change ID Database #4247
base: master
Are you sure you want to change the base?
Change ID Database #4247
Changes from all commits
cf48e16
a3c4abf
e26fa1c
9dd3d69
4662779
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
[package] | ||
name = "gitbutler-project-store" | ||
version = "0.0.0" | ||
edition = "2021" | ||
publish = false | ||
|
||
[lib] | ||
path = "src/lib.rs" | ||
doctest = false | ||
test = false | ||
|
||
[dev.dependencies] | ||
gitbutler-testsupport.workspace = true | ||
|
||
[dependencies] | ||
rusqlite = { version = "0.31.0", features = ["bundled", "uuid"] } | ||
anyhow = "1.0.86" | ||
uuid.workspace = true |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
use rusqlite::Connection; | ||
|
||
// TODO: rename to patches | ||
/// The changes struct provides a | ||
pub struct Changes<'l> { | ||
connection: &'l mut Connection, | ||
} | ||
|
||
impl<'l> Changes<'l> {} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
use anyhow::{anyhow, Context, Result}; | ||
use migrations::{gitbutler_migrations::gitbutler_migrations, migrator::Migrator}; | ||
use rusqlite::Connection; | ||
use std::path::Path; | ||
|
||
mod changes; | ||
mod migrations; | ||
|
||
const DATABASE_NAME: &str = "project.sqlite"; | ||
|
||
/// ProjectStore provides a light wrapper around a sqlite database | ||
struct ProjectStore { | ||
connection: Connection, | ||
} | ||
|
||
impl ProjectStore {} | ||
|
||
/// Database setup | ||
/// | ||
/// Before touching any database related code, please read https://github.com/the-lean-crate/criner/discussions/5 first. | ||
impl ProjectStore { | ||
/// Creates an instance of ProjectStore and runs any pending sqlite migrations | ||
/// gitbutler_project_directory should be the `.git/gitbutler` path of a given | ||
/// repository | ||
pub fn initialize(gitbutler_project_directory: &Path) -> Result<ProjectStore> { | ||
let database_path = gitbutler_project_directory.join(DATABASE_NAME); | ||
let database_path = database_path.to_str().ok_or(anyhow!( | ||
"Failed to get database {}", | ||
gitbutler_project_directory.display() | ||
))?; | ||
|
||
let connection = Connection::open(database_path)?; | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This whitespace puzzles me, even though I know that I had a phase where I did that too. |
||
ProjectStore::configure_connection(&connection)?; | ||
|
||
let mut project_store = ProjectStore { connection }; | ||
|
||
project_store.run_migrations()?; | ||
|
||
Ok(project_store) | ||
} | ||
|
||
/// Configures a sqlite connection to behave sensibly in a concurrent environemnt. | ||
/// | ||
/// Busy handler and pargma's have been taken from https://github.com/the-lean-crate/criner/discussions/5 | ||
/// and will help with concurrent reading and writing. | ||
/// | ||
/// This should be run before a project store is created and any other SQL is run. | ||
fn configure_connection(connection: &Connection) -> Result<()> { | ||
connection | ||
.busy_handler(Some(sleeper)) | ||
.context("Failed to set connection's busy handler")?; | ||
|
||
connection.execute_batch(" | ||
PRAGMA journal_mode = WAL; -- better write-concurrency | ||
PRAGMA synchronous = NORMAL; -- fsync only in critical moments | ||
PRAGMA wal_autocheckpoint = 1000; -- write WAL changes back every 1000 pages, for an in average 1MB WAL file. May affect readers if number is increased | ||
PRAGMA wal_checkpoint(TRUNCATE); -- free some space by truncating possibly massive WAL files from the last run. | ||
").context("Failed to set PRAGMA's for connection")?; | ||
|
||
Ok(()) | ||
} | ||
|
||
/// Calls the migrator with appropriate migrations | ||
fn run_migrations(&mut self) -> Result<()> { | ||
let mut migrator = Migrator::new(&mut self.connection); | ||
migrator.migrate(gitbutler_migrations())?; | ||
Ok(()) | ||
} | ||
} | ||
|
||
fn sleeper(attempts: i32) -> bool { | ||
println!("SQLITE_BUSY, retrying after 50ms (attempt {})", attempts); | ||
std::thread::sleep(std::time::Duration::from_millis(50)); | ||
true | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's have integration tests, that is tests for public APIs go into |
||
use super::*; | ||
|
||
#[test] | ||
fn run_migrations_should_succeed() { | ||
let connection = Connection::open_in_memory().unwrap(); | ||
|
||
ProjectStore::configure_connection(&connection).unwrap(); | ||
|
||
let mut project_store = ProjectStore { connection }; | ||
|
||
project_store.run_migrations().unwrap() | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
use super::migration::Migration; | ||
|
||
const CREATE_BASE_SCHEMA: &str = " | ||
CREATE TABLE changes ( | ||
id BLOB PRIMARY KEY NOT NULL, -- A UUID v4 | ||
is_unapplied_wip INTEGER NOT NULL, | ||
unapplied_vbranch_name TEXT, | ||
created_at INTEGER -- A unix timestamp in seconds when the record was created | ||
); | ||
CREATE TABLE commits ( | ||
sha TEXT PRIMARY KEY NOT NULL, -- A commit SHA as a base16 string | ||
created_at INTEGER, -- A unix timestamp in seconds when the record was created | ||
change_id BLOB NOT NULL, | ||
FOREIGN KEY(change_id) REFERENCES changes(change_id) | ||
); | ||
"; | ||
|
||
pub(crate) fn gitbutler_migrations() -> Vec<Migration> { | ||
let base_migration = Migration { | ||
name: "base".to_string(), | ||
up: |connection| { | ||
connection.execute_batch(CREATE_BASE_SCHEMA)?; | ||
|
||
Ok(()) | ||
}, | ||
}; | ||
|
||
vec![base_migration] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
use anyhow::Result; | ||
use rusqlite::Connection; | ||
|
||
pub(crate) struct Migration { | ||
/// A unique identifier for the migration | ||
pub name: String, | ||
/// A function which performs the migration. The up function gets run inside | ||
/// of a transaction. | ||
pub up: fn(&Connection) -> Result<()>, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since
Connection
is!Sync
, i.e.not Sync
, the wholeProjectStore
will have to be behind aMutex
to compensate for that.If
Clone
is implemented such that it connects to the database without rerunning initialization and migration then it concurrency will be possible nonetheless, and it can be as granular assqlite
can make it.