feat: add initial db connection and layout
This commit is contained in:
2
migrations/0001_initial.down.sql
Normal file
2
migrations/0001_initial.down.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
DROP TABLE IF EXISTS todo_tags;
|
||||||
|
DROP TABLE IF EXISTS todos;
|
||||||
14
migrations/0001_initial.up.sql
Normal file
14
migrations/0001_initial.up.sql
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS todos (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
text TEXT NOT NULL,
|
||||||
|
priority TEXT NOT NULL DEFAULT 'medium',
|
||||||
|
project TEXT,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
|
||||||
|
completed_at TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS todo_tags (
|
||||||
|
todo_id INTEGER NOT NULL REFERENCES todos(id) ON DELETE CASCADE,
|
||||||
|
tag TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (todo_id, tag)
|
||||||
|
);
|
||||||
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() {
|
mod db;
|
||||||
println!("Hello, world!");
|
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