misc: initial code
This commit is contained in:
10
crates/codetaker-db/Cargo.toml
Normal file
10
crates/codetaker-db/Cargo.toml
Normal file
@@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "codetaker-db"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
diesel.workspace = true
|
||||
chrono.workspace = true
|
||||
diesel-async.workspace = true
|
||||
diesel_migrations.workspace = true
|
||||
9
crates/codetaker-db/diesel.toml
Normal file
9
crates/codetaker-db/diesel.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
# For documentation on how to configure this file,
|
||||
# see https://diesel.rs/guides/configuring-diesel-cli
|
||||
|
||||
[print_schema]
|
||||
file = "src/schema.rs"
|
||||
custom_type_derives = ["diesel::query_builder::QueryId", "Clone"]
|
||||
|
||||
[migrations_directory]
|
||||
dir = "migrations"
|
||||
0
crates/codetaker-db/migrations/.diesel_lock
Normal file
0
crates/codetaker-db/migrations/.diesel_lock
Normal file
0
crates/codetaker-db/migrations/.keep
Normal file
0
crates/codetaker-db/migrations/.keep
Normal file
@@ -0,0 +1,5 @@
|
||||
DROP TABLE IF EXISTS review_thread_messages;
|
||||
DROP TABLE IF EXISTS review_threads;
|
||||
DROP TABLE IF EXISTS project_memory_summaries;
|
||||
DROP TABLE IF EXISTS project_memory_entries;
|
||||
DROP TABLE IF EXISTS projects;
|
||||
@@ -0,0 +1,56 @@
|
||||
CREATE TABLE projects (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
forge TEXT NOT NULL,
|
||||
owner TEXT NOT NULL,
|
||||
repo TEXT NOT NULL,
|
||||
UNIQUE(forge, owner, repo)
|
||||
);
|
||||
|
||||
CREATE TABLE project_memory_entries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
project_id INTEGER NOT NULL,
|
||||
key TEXT NOT NULL,
|
||||
value_json TEXT NOT NULL,
|
||||
source TEXT NOT NULL,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE,
|
||||
UNIQUE(project_id, key)
|
||||
);
|
||||
|
||||
CREATE TABLE project_memory_summaries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
project_id INTEGER NOT NULL,
|
||||
summary_type TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE,
|
||||
UNIQUE(project_id, summary_type)
|
||||
);
|
||||
|
||||
CREATE TABLE review_threads (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
project_id INTEGER NOT NULL,
|
||||
file TEXT NOT NULL,
|
||||
line INTEGER NOT NULL,
|
||||
initial_comment TEXT NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE review_thread_messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
thread_id INTEGER NOT NULL,
|
||||
author TEXT NOT NULL,
|
||||
body TEXT NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY(thread_id) REFERENCES review_threads(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_project_memory_entries_project_id
|
||||
ON project_memory_entries(project_id);
|
||||
CREATE INDEX idx_project_memory_summaries_project_id
|
||||
ON project_memory_summaries(project_id);
|
||||
CREATE INDEX idx_review_threads_project_id
|
||||
ON review_threads(project_id);
|
||||
CREATE INDEX idx_review_thread_messages_thread_id
|
||||
ON review_thread_messages(thread_id);
|
||||
119
crates/codetaker-db/src/db.rs
Normal file
119
crates/codetaker-db/src/db.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use diesel::{Connection as _, SqliteConnection, connection::SimpleConnection as _};
|
||||
use diesel_async::{
|
||||
AsyncConnection, SimpleAsyncConnection,
|
||||
pooled_connection::{AsyncDieselConnectionManager, ManagerConfig},
|
||||
sync_connection_wrapper::SyncConnectionWrapper,
|
||||
};
|
||||
use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations};
|
||||
|
||||
pub type DatabaseConnection = SyncConnectionWrapper<SqliteConnection>;
|
||||
pub type DatabasePool = diesel_async::pooled_connection::bb8::Pool<DatabaseConnection>;
|
||||
pub type DatabaseResult<T> = Result<T, DatabaseError>;
|
||||
|
||||
const DB_FILE: &str = "codetaker.sqlite";
|
||||
pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DatabaseError {
|
||||
Connection { message: String },
|
||||
Setup { message: String },
|
||||
Migration { message: String },
|
||||
Pool { message: String },
|
||||
}
|
||||
|
||||
impl Display for DatabaseError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Connection { message } => write!(f, "database connection error: {message}"),
|
||||
Self::Setup { message } => write!(f, "database setup error: {message}"),
|
||||
Self::Migration { message } => write!(f, "database migration error: {message}"),
|
||||
Self::Pool { message } => write!(f, "database pool error: {message}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for DatabaseError {}
|
||||
|
||||
fn database_path() -> DatabaseResult<PathBuf> {
|
||||
let cwd = std::env::current_dir().map_err(|err| DatabaseError::Setup {
|
||||
message: format!("failed to determine current directory for database path: {err}"),
|
||||
})?;
|
||||
Ok(cwd.join(DB_FILE))
|
||||
}
|
||||
|
||||
fn db_config(conn: &mut SqliteConnection) -> Result<(), diesel::result::Error> {
|
||||
conn.batch_execute("PRAGMA synchronous = NORMAL;")?;
|
||||
conn.batch_execute("PRAGMA wal_autocheckpoint = 1000;")?;
|
||||
conn.batch_execute("PRAGMA wal_checkpoint(TRUNCATE);")?;
|
||||
conn.batch_execute("PRAGMA foreign_keys = ON;")?;
|
||||
conn.batch_execute("PRAGMA auto_vacuum = FULL;")?;
|
||||
conn.batch_execute("PRAGMA secure_delete = ON;")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn initialize_database(url: &str) -> DatabaseResult<()> {
|
||||
let mut conn = SqliteConnection::establish(url).map_err(|err| DatabaseError::Connection {
|
||||
message: format!("failed to establish sqlite connection '{url}': {err}"),
|
||||
})?;
|
||||
|
||||
db_config(&mut conn).map_err(|err| DatabaseError::Setup {
|
||||
message: format!("failed to configure sqlite pragmas for '{url}': {err}"),
|
||||
})?;
|
||||
|
||||
conn.run_pending_migrations(MIGRATIONS)
|
||||
.map_err(|err| DatabaseError::Migration {
|
||||
message: format!("failed to run migrations for '{url}': {err}"),
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn create_pool(url: Option<&str>) -> DatabaseResult<DatabasePool> {
|
||||
let database_url = match url {
|
||||
Some(value) => value.to_owned(),
|
||||
None => database_path()?.to_string_lossy().into_owned(),
|
||||
};
|
||||
|
||||
initialize_database(&database_url)?;
|
||||
|
||||
let mut config = ManagerConfig::default();
|
||||
config.custom_setup = Box::new(|url| {
|
||||
Box::pin(async move {
|
||||
let mut conn = DatabaseConnection::establish(url).await?;
|
||||
conn.batch_execute("PRAGMA busy_timeout = 9000;")
|
||||
.await
|
||||
.map_err(diesel::ConnectionError::CouldntSetupConfiguration)?;
|
||||
conn.batch_execute("PRAGMA journal_mode = WAL;")
|
||||
.await
|
||||
.map_err(diesel::ConnectionError::CouldntSetupConfiguration)?;
|
||||
conn.batch_execute("PRAGMA foreign_keys = ON;")
|
||||
.await
|
||||
.map_err(diesel::ConnectionError::CouldntSetupConfiguration)?;
|
||||
Ok(conn)
|
||||
})
|
||||
});
|
||||
|
||||
DatabasePool::builder()
|
||||
.build(AsyncDieselConnectionManager::new_with_config(
|
||||
database_url,
|
||||
config,
|
||||
))
|
||||
.await
|
||||
.map_err(|err| DatabaseError::Pool {
|
||||
message: format!("failed to create database pool: {err}"),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn create_test_pool() -> DatabaseResult<DatabasePool> {
|
||||
let tempfile_name = format!(
|
||||
"codetaker_test_{}_{}.sqlite",
|
||||
std::process::id(),
|
||||
chrono::Utc::now().timestamp_nanos_opt().unwrap_or_default(),
|
||||
);
|
||||
let file = std::env::temp_dir().join(tempfile_name);
|
||||
let url = format!("{}?mode=rwc", file.to_string_lossy());
|
||||
create_pool(Some(&url)).await
|
||||
}
|
||||
9
crates/codetaker-db/src/lib.rs
Normal file
9
crates/codetaker-db/src/lib.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
mod db;
|
||||
|
||||
pub use db::{
|
||||
DatabaseConnection, DatabaseError, DatabasePool, DatabaseResult, MIGRATIONS, create_pool,
|
||||
create_test_pool,
|
||||
};
|
||||
|
||||
pub mod models;
|
||||
pub mod schema;
|
||||
108
crates/codetaker-db/src/models.rs
Normal file
108
crates/codetaker-db/src/models.rs
Normal file
@@ -0,0 +1,108 @@
|
||||
use chrono::NaiveDateTime;
|
||||
use diesel::{Associations, Identifiable, Insertable, Queryable, Selectable};
|
||||
|
||||
use crate::schema::{
|
||||
project_memory_entries, project_memory_summaries, projects, review_thread_messages,
|
||||
review_threads,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Queryable, Selectable, Identifiable)]
|
||||
#[diesel(table_name = projects)]
|
||||
pub struct ProjectRow {
|
||||
pub id: i32,
|
||||
pub forge: String,
|
||||
pub owner: String,
|
||||
pub repo: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Insertable)]
|
||||
#[diesel(table_name = projects)]
|
||||
pub struct NewProjectRow<'a> {
|
||||
pub forge: &'a str,
|
||||
pub owner: &'a str,
|
||||
pub repo: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Queryable, Selectable, Identifiable, Associations)]
|
||||
#[diesel(table_name = project_memory_entries)]
|
||||
#[diesel(belongs_to(ProjectRow, foreign_key = project_id))]
|
||||
pub struct ProjectMemoryEntryRow {
|
||||
pub id: i32,
|
||||
pub project_id: i32,
|
||||
pub key: String,
|
||||
pub value_json: String,
|
||||
pub source: String,
|
||||
pub updated_at: NaiveDateTime,
|
||||
}
|
||||
|
||||
#[derive(Debug, Insertable)]
|
||||
#[diesel(table_name = project_memory_entries)]
|
||||
pub struct NewProjectMemoryEntryRow<'a> {
|
||||
pub project_id: i32,
|
||||
pub key: &'a str,
|
||||
pub value_json: &'a str,
|
||||
pub source: &'a str,
|
||||
pub updated_at: NaiveDateTime,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Queryable, Selectable, Identifiable, Associations)]
|
||||
#[diesel(table_name = project_memory_summaries)]
|
||||
#[diesel(belongs_to(ProjectRow, foreign_key = project_id))]
|
||||
pub struct ProjectMemorySummaryRow {
|
||||
pub id: i32,
|
||||
pub project_id: i32,
|
||||
pub summary_type: String,
|
||||
pub content: String,
|
||||
pub updated_at: NaiveDateTime,
|
||||
}
|
||||
|
||||
#[derive(Debug, Insertable)]
|
||||
#[diesel(table_name = project_memory_summaries)]
|
||||
pub struct NewProjectMemorySummaryRow<'a> {
|
||||
pub project_id: i32,
|
||||
pub summary_type: &'a str,
|
||||
pub content: &'a str,
|
||||
pub updated_at: NaiveDateTime,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Queryable, Selectable, Identifiable, Associations)]
|
||||
#[diesel(table_name = review_threads)]
|
||||
#[diesel(belongs_to(ProjectRow, foreign_key = project_id))]
|
||||
pub struct ReviewThreadRow {
|
||||
pub id: i32,
|
||||
pub project_id: i32,
|
||||
pub file: String,
|
||||
pub line: i32,
|
||||
pub initial_comment: String,
|
||||
pub created_at: NaiveDateTime,
|
||||
}
|
||||
|
||||
#[derive(Debug, Insertable)]
|
||||
#[diesel(table_name = review_threads)]
|
||||
pub struct NewReviewThreadRow<'a> {
|
||||
pub project_id: i32,
|
||||
pub file: &'a str,
|
||||
pub line: i32,
|
||||
pub initial_comment: &'a str,
|
||||
pub created_at: NaiveDateTime,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Queryable, Selectable, Identifiable, Associations)]
|
||||
#[diesel(table_name = review_thread_messages)]
|
||||
#[diesel(belongs_to(ReviewThreadRow, foreign_key = thread_id))]
|
||||
pub struct ReviewThreadMessageRow {
|
||||
pub id: i32,
|
||||
pub thread_id: i32,
|
||||
pub author: String,
|
||||
pub body: String,
|
||||
pub created_at: NaiveDateTime,
|
||||
}
|
||||
|
||||
#[derive(Debug, Insertable)]
|
||||
#[diesel(table_name = review_thread_messages)]
|
||||
pub struct NewReviewThreadMessageRow<'a> {
|
||||
pub thread_id: i32,
|
||||
pub author: &'a str,
|
||||
pub body: &'a str,
|
||||
pub created_at: NaiveDateTime,
|
||||
}
|
||||
65
crates/codetaker-db/src/schema.rs
Normal file
65
crates/codetaker-db/src/schema.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
// @generated automatically by Diesel CLI-like configuration. Kept explicit in-repo for agent tests.
|
||||
|
||||
diesel::table! {
|
||||
projects (id) {
|
||||
id -> Integer,
|
||||
forge -> Text,
|
||||
owner -> Text,
|
||||
repo -> Text,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
project_memory_entries (id) {
|
||||
id -> Integer,
|
||||
project_id -> Integer,
|
||||
key -> Text,
|
||||
value_json -> Text,
|
||||
source -> Text,
|
||||
updated_at -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
project_memory_summaries (id) {
|
||||
id -> Integer,
|
||||
project_id -> Integer,
|
||||
summary_type -> Text,
|
||||
content -> Text,
|
||||
updated_at -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
review_thread_messages (id) {
|
||||
id -> Integer,
|
||||
thread_id -> Integer,
|
||||
author -> Text,
|
||||
body -> Text,
|
||||
created_at -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
review_threads (id) {
|
||||
id -> Integer,
|
||||
project_id -> Integer,
|
||||
file -> Text,
|
||||
line -> Integer,
|
||||
initial_comment -> Text,
|
||||
created_at -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::joinable!(project_memory_entries -> projects (project_id));
|
||||
diesel::joinable!(project_memory_summaries -> projects (project_id));
|
||||
diesel::joinable!(review_thread_messages -> review_threads (thread_id));
|
||||
diesel::joinable!(review_threads -> projects (project_id));
|
||||
|
||||
diesel::allow_tables_to_appear_in_same_query!(
|
||||
projects,
|
||||
project_memory_entries,
|
||||
project_memory_summaries,
|
||||
review_threads,
|
||||
review_thread_messages,
|
||||
);
|
||||
Reference in New Issue
Block a user