// Copyright (c) 2021, 2022 Teddy Wing
//
// This file is part of Reflectub.
//
// Reflectub is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Reflectub is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Reflectub. If not, see .
use r2d2;
use r2d2_sqlite::SqliteConnectionManager;
use rusqlite::{self, OptionalExtension};
use thiserror;
use crate::github;
/// Repository metadata mapped to the database.
#[derive(Debug)]
pub struct Repo {
id: i64,
name: Option,
description: Option,
pub default_branch: Option,
updated_at: Option,
}
impl Repo {
pub fn description(&self) -> &str {
self.description
.as_deref()
.unwrap_or("")
}
}
impl From<&github::Repo> for Repo {
fn from(repo: &github::Repo) -> Self {
use chrono::DateTime;
let repo_updated_at = DateTime::parse_from_rfc3339(&repo.updated_at).ok();
let repo_pushed_at = DateTime::parse_from_rfc3339(&repo.pushed_at).ok();
// Set `updated_at` to the most recent of `repo_updated_at` or
// `repo_pushed_at`.
let updated_at =
if repo_updated_at.is_none() && repo_pushed_at.is_none() {
repo.updated_at.clone()
// `repo_updated_at` and `repo_pushed_at` are both Some.
} else if repo_pushed_at.unwrap() > repo_updated_at.unwrap() {
repo.pushed_at.clone()
// Default to `repo.updated_at`.
} else {
repo.updated_at.clone()
};
Self {
id: repo.id,
name: Some(repo.name.clone()),
description: repo.description.clone(),
default_branch: Some(repo.default_branch.clone()),
updated_at: Some(updated_at),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("database error")]
Db(#[from] rusqlite::Error),
#[error("connection pool error")]
Pool(#[from] r2d2::Error),
}
#[derive(Debug)]
pub struct Db {
pool: r2d2::Pool,
}
impl Db {
/// Open a connection to the database.
pub fn connect(path: &str) -> Result {
let manager = SqliteConnectionManager::file(path)
.with_flags(
rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE
| rusqlite::OpenFlags::SQLITE_OPEN_CREATE,
);
Ok(
Db {
pool: r2d2::Pool::new(manager)?,
}
)
}
/// Initialise the database with tables and indexes.
pub fn create(&self) -> Result<(), Error> {
let mut pool = self.pool.get()?;
let tx = pool.transaction()?;
tx.execute(
r#"
CREATE TABLE IF NOT EXISTS repositories (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
default_branch TEXT,
updated_at TEXT NOT NULL
);
"#,
[],
)?;
tx.execute(
r#"
CREATE UNIQUE INDEX IF NOT EXISTS idx_repositories_id
ON repositories (id);
"#,
[],
)?;
tx.commit()?;
Ok(())
}
/// Get a repository by its ID.
///
/// Returns a `rusqlite::Error::QueryReturnedNoRows` error if the row
/// doesn't exist.
pub fn repo_get(&self, id: i64) -> Result {
let mut pool = self.pool.get()?;
let tx = pool.transaction()?;
let repo = tx.query_row(
r#"
SELECT
id,
name,
description,
default_branch,
updated_at
FROM repositories
WHERE id = ?
"#,
[id],
|row| {
Ok(
Repo {
id: row.get(0)?,
name: Some(row.get(1)?),
description: row.get(2)?,
default_branch: row.get(3)?,
updated_at: Some(row.get(4)?),
}
)
},
)?;
tx.commit()?;
Ok(repo)
}
/// Insert a new repository.
pub fn repo_insert(&self, repo: Repo) -> Result<(), Error> {
let mut pool = self.pool.get()?;
let tx = pool.transaction()?;
tx.execute(
r#"
INSERT INTO repositories
(id, name, description, default_branch, updated_at)
VALUES
(?, ?, ?, ?, ?)
"#,
rusqlite::params![
repo.id,
&repo.name,
&repo.description,
&repo.default_branch,
&repo.updated_at,
],
)?;
tx.commit()?;
Ok(())
}
/// Check if the given repository is newer than the one in the repository.
///
/// Compares the `updated_at` field to find out whether the repository was
/// updated.
pub fn repo_is_updated(
&self,
repo: &Repo,
) -> Result {
let mut pool = self.pool.get()?;
let tx = pool.transaction()?;
let is_updated = match tx.query_row(
r#"
SELECT 1
FROM repositories
WHERE id = ?
AND datetime(updated_at) < datetime(?)
"#,
rusqlite::params![
repo.id,
&repo.updated_at,
],
|row| row.get::(0),
)
.optional()
{
Ok(Some(_)) => Ok(true),
Ok(None) => Ok(false),
Err(e) => Err(e.into()),
};
tx.commit()?;
is_updated
}
/// Update an existing repository.
pub fn repo_update(&self, repo: &Repo) -> Result<(), Error> {
let mut pool = self.pool.get()?;
let tx = pool.transaction()?;
tx.execute(
r#"
UPDATE repositories
SET
name = ?,
description = ?,
default_branch = ?,
updated_at = ?
WHERE id = ?
"#,
rusqlite::params![
&repo.name,
&repo.description,
&repo.default_branch,
&repo.updated_at,
repo.id,
],
)?;
tx.commit()?;
Ok(())
}
}