feat: add initial db connection and layout
This commit is contained in:
151
src/db.rs
Normal file
151
src/db.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
10
src/main.rs
10
src/main.rs
@@ -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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user