bmo 0.5.0

Local-first SQLite-backed CLI issue tracker for AI agents
Documentation
use sea_query::{Expr, ExprTrait, Iden, OnConflict, Query, SqliteQueryBuilder};
use sea_query_rusqlite::{RusqliteBinder, rusqlite};

use crate::model::activity::NewActivityEntry;
use crate::model::{
    ActivityEntry, Comment, Issue, IssueFile, IssueFilter, Label, Relation, RelationKind, Status,
};

use super::{
    AddCommentInput, CreateIssueInput, Repository, SqliteRepository, Stats, UpdateIssueInput,
};

/// Identifier enum for the `meta` key-value table.
#[derive(Iden)]
enum Meta {
    Table,
    Key,
    Value,
}

impl Repository for SqliteRepository {
    fn create_issue(&self, input: &CreateIssueInput) -> anyhow::Result<Issue> {
        self.create_issue_impl(input)
    }

    fn get_issue(&self, id: i64) -> anyhow::Result<Option<Issue>> {
        self.get_issue_impl(id)
    }

    fn list_issues(&self, filter: IssueFilter) -> anyhow::Result<Vec<Issue>> {
        self.list_issues_impl(filter)
    }

    fn count_issues(&self, filter: IssueFilter) -> anyhow::Result<i64> {
        self.count_issues_impl(filter)
    }

    fn update_issue(&self, id: i64, input: &UpdateIssueInput) -> anyhow::Result<Issue> {
        self.update_issue_impl(id, input)
    }

    fn delete_issue(&self, id: i64) -> anyhow::Result<()> {
        self.delete_issue_impl(id)
    }

    fn truncate_issues(&self, statuses: &[Status]) -> anyhow::Result<u64> {
        self.truncate_issues_impl(statuses)
    }

    fn truncate_all_issues(&self) -> anyhow::Result<u64> {
        self.truncate_all_issues_impl()
    }

    fn get_sub_issues(&self, parent_id: i64) -> anyhow::Result<Vec<Issue>> {
        self.get_sub_issues_impl(parent_id)
    }

    fn add_comment(&self, input: &AddCommentInput) -> anyhow::Result<Comment> {
        self.add_comment_impl(input)
    }

    fn list_comments(&self, issue_id: i64) -> anyhow::Result<Vec<Comment>> {
        self.list_comments_impl(issue_id)
    }

    fn get_or_create_label(&self, name: &str, color: Option<&str>) -> anyhow::Result<Label> {
        self.get_or_create_label_impl(name, color)
    }

    fn add_label_to_issue(&self, issue_id: i64, label_id: i64) -> anyhow::Result<()> {
        self.add_label_to_issue_impl(issue_id, label_id)
    }

    fn remove_label_from_issue(&self, issue_id: i64, label_name: &str) -> anyhow::Result<()> {
        self.remove_label_from_issue_impl(issue_id, label_name)
    }

    fn list_issue_labels(&self, issue_id: i64) -> anyhow::Result<Vec<Label>> {
        self.list_issue_labels_impl(issue_id)
    }

    fn list_all_labels(&self) -> anyhow::Result<Vec<Label>> {
        self.list_all_labels_impl()
    }

    fn delete_label(&self, name: &str) -> anyhow::Result<()> {
        self.delete_label_impl(name)
    }

    fn add_relation(
        &self,
        from_id: i64,
        kind: RelationKind,
        to_id: i64,
    ) -> anyhow::Result<Relation> {
        self.add_relation_impl(from_id, kind, to_id)
    }

    fn remove_relation(&self, relation_id: i64) -> anyhow::Result<()> {
        self.remove_relation_impl(relation_id)
    }

    fn list_relations(&self, issue_id: i64) -> anyhow::Result<Vec<Relation>> {
        self.list_relations_impl(issue_id)
    }

    fn list_all_relations(&self) -> anyhow::Result<Vec<Relation>> {
        self.list_all_relations_impl()
    }

    fn log_activity(&self, entry: &NewActivityEntry) -> anyhow::Result<()> {
        self.log_activity_impl(entry)
    }

    fn list_activity(&self, issue_id: i64, limit: usize) -> anyhow::Result<Vec<ActivityEntry>> {
        self.list_activity_impl(issue_id, limit)
    }

    fn add_file(&self, issue_id: i64, path: &str) -> anyhow::Result<IssueFile> {
        self.add_file_impl(issue_id, path)
    }

    fn remove_file(&self, issue_id: i64, path: &str) -> anyhow::Result<()> {
        self.remove_file_impl(issue_id, path)
    }

    fn list_files(&self, issue_id: i64) -> anyhow::Result<Vec<IssueFile>> {
        self.list_files_impl(issue_id)
    }

    fn list_file_conflicts(
        &self,
        issue_id: i64,
    ) -> anyhow::Result<Vec<crate::model::FileConflict>> {
        self.list_file_conflicts_impl(issue_id)
    }

    fn claim_issue(
        &self,
        input: &crate::db::ClaimIssueInput,
    ) -> anyhow::Result<crate::model::Issue> {
        self.claim_issue_impl(input)
    }

    fn get_meta(&self, key: &str) -> anyhow::Result<Option<String>> {
        let (sql, values) = Query::select()
            .column(Meta::Value)
            .from(Meta::Table)
            .and_where(Expr::col(Meta::Key).eq(key))
            .build_rusqlite(SqliteQueryBuilder);
        let result = self
            .conn
            .query_row(sql.as_str(), &*values.as_params(), |r| r.get(0));
        match result {
            Ok(v) => Ok(Some(v)),
            Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
            Err(e) => Err(e.into()),
        }
    }

    fn set_meta(&self, key: &str, value: &str) -> anyhow::Result<()> {
        let (sql, values) = Query::insert()
            .into_table(Meta::Table)
            .columns([Meta::Key, Meta::Value])
            .values_panic([key.into(), value.into()])
            .on_conflict(
                OnConflict::column(Meta::Key)
                    .update_column(Meta::Value)
                    .to_owned(),
            )
            .build_rusqlite(SqliteQueryBuilder);
        self.conn.execute(sql.as_str(), &*values.as_params())?;
        Ok(())
    }

    fn get_stats(&self) -> anyhow::Result<Stats> {
        self.get_stats_impl()
    }

    fn board_snapshot_stats(&self) -> anyhow::Result<(i64, Option<chrono::DateTime<chrono::Utc>>)> {
        self.board_snapshot_stats_impl()
    }

    fn list_issues_by_status(
        &self,
        limit_per_status: usize,
    ) -> anyhow::Result<std::collections::HashMap<crate::model::Status, Vec<Issue>>> {
        self.list_issues_by_status_impl(limit_per_status)
    }
}