Skip to content
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

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ members = [
"crates/gitbutler-watcher/vendor/debouncer",
"crates/gitbutler-testsupport",
"crates/gitbutler-cli",
"crates/gitbutler-project-store"
]
resolver = "2"

[workspace.dependencies]
# Add the `tracing` or `tracing-detail` features to see more of gitoxide in the logs. Useful to see which programs it invokes.
gix = { git = "https://github.com/Byron/gitoxide", rev = "55cbc1b9d6f210298a86502a7f20f9736c7e963e", default-features = false, features = [] }
git2 = { version = "0.18.3", features = ["vendored-openssl", "vendored-libgit2"] }
uuid = { version = "1.8.0", features = ["serde"] }
uuid = { version = "1.8.0", features = ["serde", "v4", "v7"] }
serde = { version = "1.0", features = ["derive"] }
thiserror = "1.0.61"
tokio = { version = "1.38.0", default-features = false }
Expand All @@ -25,6 +26,7 @@ gitbutler-core = { path = "crates/gitbutler-core" }
gitbutler-watcher = { path = "crates/gitbutler-watcher" }
gitbutler-testsupport = { path = "crates/gitbutler-testsupport" }
gitbutler-cli ={ path = "crates/gitbutler-cli" }
gitbutler-project-store = { path = "crates/gitbutler-project-store" }

[profile.release]
codegen-units = 1 # Compile crates one after another so the compiler can optimize better
Expand Down
1 change: 1 addition & 0 deletions crates/gitbutler-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ uuid.workspace = true
walkdir = "2.5.0"
zip = "0.6.5"
gitbutler-git.workspace = true
gitbutler-project-store.workspace = true

[features]
# by default Tauri runs in production mode
Expand Down
18 changes: 18 additions & 0 deletions crates/gitbutler-project-store/Cargo.toml
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
9 changes: 9 additions & 0 deletions crates/gitbutler-project-store/src/changes.rs
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> {}
92 changes: 92 additions & 0 deletions crates/gitbutler-project-store/src/lib.rs
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 {
Copy link
Collaborator

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 whole ProjectStore will have to be behind a Mutex 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 as sqlite can make it.

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)?;

Copy link
Collaborator

Choose a reason for hiding this comment

The 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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 <crate>/tests/ similar to how it's done in core. It's actually a good check to assure public APIs are actually usable from other crates as well.

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]
}
10 changes: 10 additions & 0 deletions crates/gitbutler-project-store/src/migrations/migration.rs
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<()>,
}
Loading
Loading