Add database schema and migration
Assisted-by: Claude Opus 4.6 via pi
change krrxwwsnpuvrzzwlyzzvysxyzooxkykx
commit 081322c8ca6fda8ecf2abddcb170419eb76f3c14
author Alpha Chen <alpha@kejadlen.dev>
date
parent xnkxvlpt
diff --git a/crates/ranger-lib/migrations/001_initial.sql b/crates/ranger-lib/migrations/001_initial.sql
new file mode 100644
index 0000000..8324dd8
--- /dev/null
+++ b/crates/ranger-lib/migrations/001_initial.sql
@@ -0,0 +1,50 @@
+CREATE TABLE IF NOT EXISTS backlogs (
+    id INTEGER PRIMARY KEY AUTOINCREMENT,
+    key TEXT NOT NULL UNIQUE,
+    name TEXT NOT NULL,
+    created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
+    updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
+);
+
+CREATE TABLE IF NOT EXISTS tasks (
+    id INTEGER PRIMARY KEY AUTOINCREMENT,
+    key TEXT NOT NULL UNIQUE,
+    parent_id INTEGER REFERENCES tasks(id) ON DELETE CASCADE,
+    title TEXT NOT NULL,
+    description TEXT,
+    state TEXT NOT NULL DEFAULT 'icebox' CHECK(state IN ('icebox', 'queued', 'in_progress', 'done')),
+    created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
+    updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
+);
+
+CREATE TABLE IF NOT EXISTS backlog_tasks (
+    backlog_id INTEGER NOT NULL REFERENCES backlogs(id) ON DELETE CASCADE,
+    task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
+    position TEXT NOT NULL,
+    PRIMARY KEY (backlog_id, task_id)
+);
+
+CREATE TABLE IF NOT EXISTS comments (
+    id INTEGER PRIMARY KEY AUTOINCREMENT,
+    task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
+    body TEXT NOT NULL,
+    created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
+);
+
+CREATE TABLE IF NOT EXISTS blockers (
+    id INTEGER PRIMARY KEY AUTOINCREMENT,
+    task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
+    blocked_by_task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
+    UNIQUE(task_id, blocked_by_task_id)
+);
+
+CREATE TABLE IF NOT EXISTS tags (
+    id INTEGER PRIMARY KEY AUTOINCREMENT,
+    name TEXT NOT NULL UNIQUE
+);
+
+CREATE TABLE IF NOT EXISTS task_tags (
+    task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
+    tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
+    PRIMARY KEY (task_id, tag_id)
+);
diff --git a/crates/ranger-lib/src/db.rs b/crates/ranger-lib/src/db.rs
new file mode 100644
index 0000000..acb6e05
--- /dev/null
+++ b/crates/ranger-lib/src/db.rs
@@ -0,0 +1,61 @@
+use crate::error::RangerError;
+use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions};
+use std::path::Path;
+
+pub async fn connect(path: &Path) -> Result<SqlitePool, RangerError> {
+    if let Some(parent) = path.parent() {
+        std::fs::create_dir_all(parent)?;
+    }
+
+    let options = SqliteConnectOptions::new()
+        .filename(path)
+        .create_if_missing(true)
+        .foreign_keys(true);
+
+    let pool = SqlitePoolOptions::new()
+        .max_connections(1)
+        .connect_with(options)
+        .await?;
+
+    migrate(&pool).await?;
+    Ok(pool)
+}
+
+async fn migrate(pool: &SqlitePool) -> Result<(), RangerError> {
+    sqlx::raw_sql(include_str!("../migrations/001_initial.sql"))
+        .execute(pool)
+        .await?;
+    Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use tempfile::tempdir;
+
+    #[tokio::test]
+    async fn connect_creates_db_and_runs_migrations() {
+        let dir = tempdir().unwrap();
+        let db_path = dir.path().join("test.db");
+        let pool = connect(&db_path).await.unwrap();
+
+        let result =
+            sqlx::query("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
+                .fetch_all(&pool)
+                .await
+                .unwrap();
+
+        let table_names: Vec<String> = result
+            .iter()
+            .map(|row| sqlx::Row::get(row, "name"))
+            .collect();
+
+        assert!(table_names.contains(&"backlogs".to_string()));
+        assert!(table_names.contains(&"tasks".to_string()));
+        assert!(table_names.contains(&"backlog_tasks".to_string()));
+        assert!(table_names.contains(&"comments".to_string()));
+        assert!(table_names.contains(&"blockers".to_string()));
+        assert!(table_names.contains(&"tags".to_string()));
+        assert!(table_names.contains(&"task_tags".to_string()));
+    }
+}
diff --git a/crates/ranger-lib/src/lib.rs b/crates/ranger-lib/src/lib.rs
index 3da7711..e750927 100644
--- a/crates/ranger-lib/src/lib.rs
+++ b/crates/ranger-lib/src/lib.rs
@@ -1,3 +1,4 @@
+pub mod db;
 pub mod error;
 pub mod key;
 pub mod position;