feat: SQLite database with migration system and full schema
This commit is contained in:
parent
590bf337c0
commit
afd092c642
6 changed files with 281 additions and 1 deletions
78
src-tauri/Cargo.lock
generated
78
src-tauri/Cargo.lock
generated
|
|
@ -8,6 +8,18 @@ version = "2.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ahash"
|
||||||
|
version = "0.8.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"once_cell",
|
||||||
|
"version_check",
|
||||||
|
"zerocopy",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "1.1.4"
|
version = "1.1.4"
|
||||||
|
|
@ -307,8 +319,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
|
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"iana-time-zone",
|
"iana-time-zone",
|
||||||
|
"js-sys",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"serde",
|
"serde",
|
||||||
|
"wasm-bindgen",
|
||||||
"windows-link 0.2.1",
|
"windows-link 0.2.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -714,6 +728,18 @@ dependencies = [
|
||||||
"typeid",
|
"typeid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fallible-iterator"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fallible-streaming-iterator"
|
||||||
|
version = "0.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastrand"
|
name = "fastrand"
|
||||||
version = "2.4.1"
|
version = "2.4.1"
|
||||||
|
|
@ -1209,6 +1235,15 @@ version = "0.12.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.14.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||||
|
dependencies = [
|
||||||
|
"ahash",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.15.5"
|
version = "0.15.5"
|
||||||
|
|
@ -1224,6 +1259,15 @@ version = "0.17.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
|
checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashlink"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
|
||||||
|
dependencies = [
|
||||||
|
"hashbrown 0.14.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
|
|
@ -1728,6 +1772,17 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libsqlite3-sys"
|
||||||
|
version = "0.28.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"pkg-config",
|
||||||
|
"vcpkg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "litemap"
|
name = "litemap"
|
||||||
version = "0.8.2"
|
version = "0.8.2"
|
||||||
|
|
@ -2078,10 +2133,13 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||||
name = "orchai"
|
name = "orchai"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"rusqlite",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2691,6 +2749,20 @@ dependencies = [
|
||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rusqlite"
|
||||||
|
version = "0.31.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.11.0",
|
||||||
|
"fallible-iterator",
|
||||||
|
"fallible-streaming-iterator",
|
||||||
|
"hashlink",
|
||||||
|
"libsqlite3-sys",
|
||||||
|
"smallvec",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-hash"
|
name = "rustc-hash"
|
||||||
version = "2.1.2"
|
version = "2.1.2"
|
||||||
|
|
@ -3939,6 +4011,12 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vcpkg"
|
||||||
|
version = "0.2.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version-compare"
|
name = "version-compare"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,9 @@ tauri-build = { version = "2", features = [] }
|
||||||
tauri = { version = "2", features = [] }
|
tauri = { version = "2", features = [] }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
rusqlite = { version = "0.31", features = ["bundled"] }
|
||||||
|
uuid = { version = "1", features = ["v4", "serde"] }
|
||||||
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
incremental = true # Compiles your binary in smaller steps.
|
incremental = true # Compiles your binary in smaller steps.
|
||||||
|
|
|
||||||
61
src-tauri/migrations/001_init.sql
Normal file
61
src-tauri/migrations/001_init.sql
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS projects (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
path TEXT NOT NULL,
|
||||||
|
cloned_from TEXT,
|
||||||
|
base_branch TEXT NOT NULL DEFAULT 'main',
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS tuleap_credentials (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
tuleap_url TEXT NOT NULL,
|
||||||
|
username TEXT NOT NULL,
|
||||||
|
password_encrypted TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS watched_trackers (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||||||
|
tracker_id INTEGER NOT NULL,
|
||||||
|
tracker_label TEXT NOT NULL,
|
||||||
|
polling_interval INTEGER NOT NULL DEFAULT 10,
|
||||||
|
agent_config_json TEXT NOT NULL,
|
||||||
|
filters_json TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS processed_tickets (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
tracker_id TEXT NOT NULL REFERENCES watched_trackers(id) ON DELETE CASCADE,
|
||||||
|
artifact_id INTEGER NOT NULL,
|
||||||
|
artifact_title TEXT NOT NULL,
|
||||||
|
artifact_data TEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL DEFAULT 'Pending',
|
||||||
|
analyst_report TEXT,
|
||||||
|
developer_report TEXT,
|
||||||
|
detected_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
processed_at TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS worktrees (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
ticket_id TEXT NOT NULL REFERENCES processed_tickets(id),
|
||||||
|
path TEXT NOT NULL,
|
||||||
|
branch_name TEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL DEFAULT 'Active',
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||||
|
merged_at TEXT,
|
||||||
|
merged_into TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS notifications (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||||||
|
ticket_id TEXT REFERENCES processed_tickets(id),
|
||||||
|
type TEXT NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
message TEXT NOT NULL,
|
||||||
|
read INTEGER NOT NULL DEFAULT 0,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
85
src-tauri/src/db.rs
Normal file
85
src-tauri/src/db.rs
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
use rusqlite::{Connection, Result};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
const MIGRATION_001: &str = include_str!("../migrations/001_init.sql");
|
||||||
|
|
||||||
|
pub fn init(db_path: &Path) -> Result<Connection> {
|
||||||
|
let conn = Connection::open(db_path)?;
|
||||||
|
configure(&conn)?;
|
||||||
|
migrate(&conn)?;
|
||||||
|
Ok(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init_in_memory() -> Result<Connection> {
|
||||||
|
let conn = Connection::open_in_memory()?;
|
||||||
|
configure(&conn)?;
|
||||||
|
migrate(&conn)?;
|
||||||
|
Ok(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configure(conn: &Connection) -> Result<()> {
|
||||||
|
conn.pragma_update(None, "journal_mode", "wal")?;
|
||||||
|
conn.pragma_update(None, "foreign_keys", "ON")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn migrate(conn: &Connection) -> Result<()> {
|
||||||
|
let version: i32 = conn.pragma_query_value(None, "user_version", |row| row.get(0))?;
|
||||||
|
|
||||||
|
if version < 1 {
|
||||||
|
conn.execute_batch(MIGRATION_001)?;
|
||||||
|
conn.pragma_update(None, "user_version", 1)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_init_in_memory_creates_tables() {
|
||||||
|
let conn = init_in_memory().expect("should initialize");
|
||||||
|
|
||||||
|
// Verify all 6 tables exist
|
||||||
|
let tables: Vec<String> = conn
|
||||||
|
.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name")
|
||||||
|
.unwrap()
|
||||||
|
.query_map([], |row| row.get(0))
|
||||||
|
.unwrap()
|
||||||
|
.collect::<Result<Vec<String>, _>>()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
tables,
|
||||||
|
vec![
|
||||||
|
"notifications",
|
||||||
|
"processed_tickets",
|
||||||
|
"projects",
|
||||||
|
"tuleap_credentials",
|
||||||
|
"watched_trackers",
|
||||||
|
"worktrees",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_init_in_memory_enables_foreign_keys() {
|
||||||
|
let conn = init_in_memory().expect("should initialize");
|
||||||
|
let fk_enabled: i32 = conn
|
||||||
|
.query_row("PRAGMA foreign_keys", [], |row| row.get(0))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(fk_enabled, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_migration_is_idempotent() {
|
||||||
|
let conn = init_in_memory().expect("should initialize");
|
||||||
|
// Running init again on same connection should not fail
|
||||||
|
let version: i32 = conn
|
||||||
|
.pragma_query_value(None, "user_version", |row| row.get(0))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(version, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
34
src-tauri/src/error.rs
Normal file
34
src-tauri/src/error.rs
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct AppError {
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<rusqlite::Error> for AppError {
|
||||||
|
fn from(e: rusqlite::Error) -> Self {
|
||||||
|
AppError {
|
||||||
|
message: e.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::io::Error> for AppError {
|
||||||
|
fn from(e: std::io::Error) -> Self {
|
||||||
|
AppError {
|
||||||
|
message: e.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for AppError {
|
||||||
|
fn from(s: String) -> Self {
|
||||||
|
AppError { message: s }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for AppError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,26 @@
|
||||||
|
mod db;
|
||||||
|
mod error;
|
||||||
|
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use tauri::Manager;
|
||||||
|
|
||||||
|
pub struct AppState {
|
||||||
|
pub db: Mutex<rusqlite::Connection>,
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.invoke_handler(tauri::generate_handler![])
|
.setup(|app| {
|
||||||
|
let db_dir = app.path().app_data_dir()?;
|
||||||
|
std::fs::create_dir_all(&db_dir)?;
|
||||||
|
let db_path = db_dir.join("orchai.db");
|
||||||
|
let conn = db::init(&db_path).expect("Failed to initialize database");
|
||||||
|
app.manage(AppState {
|
||||||
|
db: Mutex::new(conn),
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue