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"
|
||||
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]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
|
|
@ -307,8 +319,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
|
|
@ -714,6 +728,18 @@ dependencies = [
|
|||
"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]]
|
||||
name = "fastrand"
|
||||
version = "2.4.1"
|
||||
|
|
@ -1209,6 +1235,15 @@ version = "0.12.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.5"
|
||||
|
|
@ -1224,6 +1259,15 @@ version = "0.17.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "heck"
|
||||
version = "0.4.1"
|
||||
|
|
@ -1728,6 +1772,17 @@ dependencies = [
|
|||
"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]]
|
||||
name = "litemap"
|
||||
version = "0.8.2"
|
||||
|
|
@ -2078,10 +2133,13 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
|||
name = "orchai"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"rusqlite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2691,6 +2749,20 @@ dependencies = [
|
|||
"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]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.2"
|
||||
|
|
@ -3939,6 +4011,12 @@ dependencies = [
|
|||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "version-compare"
|
||||
version = "0.2.1"
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@ tauri-build = { version = "2", features = [] }
|
|||
tauri = { version = "2", features = [] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
rusqlite = { version = "0.31", features = ["bundled"] }
|
||||
uuid = { version = "1", features = ["v4", "serde"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
|
||||
[profile.dev]
|
||||
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)]
|
||||
pub fn run() {
|
||||
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!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue