feat: add initial db connection and layout

This commit is contained in:
2026-02-28 12:23:24 +01:00
parent 507cc7434e
commit 00bd396bbe
4 changed files with 175 additions and 2 deletions

151
src/db.rs Normal file
View File

@@ -0,0 +1,151 @@
use std::env;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
use sqlx::SqlitePool;
use crate::model::{Priority, Todo};
pub type Pool = SqlitePool;
// resolve the global todo database path
pub fn db_path() -> Result<PathBuf> {
if let Ok(path) = env::var("TODO_DB") {
return Ok(PathBuf::from(path));
}
let data_dir = dirs::data_dir()
.context("cannot determine data directory")?
.join("todo");
std::fs::create_dir_all(&data_dir)
.with_context(|| format!("cannot create {}", data_dir.display()))?;
Ok(data_dir.join("todos.db"))
}
pub async fn connect() -> Result<Pool> {
let path = db_path()?;
let options = SqliteConnectOptions::new()
.filename(&path)
.create_if_missing(true)
.foreign_keys(true);
let pool = SqlitePoolOptions::new()
.max_connections(1)
.connect_with(options)
.await
.context("cannot open database")?;
sqlx::migrate!("./migrations")
.run(&pool)
.await
.context("cannot run migrations")?;
Ok(pool)
}
// detect git repo root by walking up from cwd
pub fn detect_project() -> Option<String> {
let cwd = env::current_dir().ok()?;
let mut dir: &Path = &cwd;
loop {
if dir.join(".git").exists() {
return Some(dir.to_string_lossy().into_owned());
}
dir = dir.parent()?;
}
}
fn priority_to_str(p: &Priority) -> &'static str {
match p {
Priority::Low => "low",
Priority::Medium => "medium",
Priority::High => "high",
Priority::Critical => "critical",
}
}
pub async fn insert_todo(
pool: &Pool,
text: &str,
priority: &Priority,
project: Option<&str>,
tags: &[String],
) -> Result<Todo> {
let pri_str = priority_to_str(priority);
let row = sqlx::query_scalar::<_, i64>(
"INSERT INTO todos (text, priority, project) VALUES (?1, ?2, ?3) RETURNING id",
)
.bind(text)
.bind(pri_str)
.bind(project)
.fetch_one(pool)
.await
.context("cannot insert todo")?;
let id = row;
for tag in tags {
sqlx::query("INSERT INTO todo_tags (todo_id, tag) VALUES (?1, ?2)")
.bind(id)
.bind(tag)
.execute(pool)
.await
.context("cannot insert tag")?;
}
// fetch the full row back
fetch_todo(pool, id).await
}
pub async fn fetch_todo(pool: &Pool, id: i64) -> Result<Todo> {
let row = sqlx::query_as::<_, TodoRow>(
"SELECT id, text, priority, project, created_at, completed_at FROM todos WHERE id = ?1",
)
.bind(id)
.fetch_optional(pool)
.await
.context("cannot fetch todo")?
.with_context(|| format!("todo {id} not found"))?;
let tags = sqlx::query_scalar::<_, String>(
"SELECT tag FROM todo_tags WHERE todo_id = ?1 ORDER BY tag",
)
.bind(id)
.fetch_all(pool)
.await
.context("cannot fetch tags")?;
Ok(row.into_todo(tags))
}
// internal row type for sqlx FromRow
#[derive(sqlx::FromRow)]
struct TodoRow {
id: i64,
text: String,
priority: String,
project: Option<String>,
created_at: chrono::DateTime<chrono::Utc>,
completed_at: Option<chrono::DateTime<chrono::Utc>>,
}
impl TodoRow {
fn into_todo(self, tags: Vec<String>) -> Todo {
let priority = self.priority.parse().unwrap_or_default();
Todo {
id: self.id,
text: self.text,
priority,
project: self.project,
tags,
created_at: self.created_at,
completed_at: self.completed_at,
}
}
}

View File

@@ -1,3 +1,9 @@
fn main() {
println!("Hello, world!");
mod db;
mod model;
#[tokio::main(flavor = "current_thread")]
async fn main() -> anyhow::Result<()> {
let _pool = db::connect().await?;
println!("database ready at {}", db::db_path()?.display());
Ok(())
}