init bookmarks package

This commit is contained in:
Nikita Galaiko 2023-05-19 10:01:01 +02:00
parent 0e89f15a62
commit 612261cc0a
4 changed files with 293 additions and 0 deletions

View File

@ -0,0 +1,270 @@
use std::ops;
use anyhow::{Context, Result};
use crate::database;
use super::Bookmark;
#[derive(Clone)]
pub struct Database {
database: database::Database,
}
impl Database {
pub fn new(database: database::Database) -> Self {
Self { database }
}
pub fn insert(&self, bookmark: &Bookmark) -> Result<()> {
self.database.transaction(|tx| -> Result<()> {
let mut stmt = insert_stmt(tx).context("Failed to prepare insert statement")?;
let timestamp_ms = bookmark.timestamp_ms.to_string();
stmt.execute(rusqlite::named_params! {
":id": &bookmark.id,
":project_id": &bookmark.project_id,
":timestamp_ms": &timestamp_ms,
":note": &bookmark.note,
})
.context("Failed to execute insert statement")?;
Ok(())
})
}
fn list_by_project_id_range(
&self,
project_id: &str,
range: ops::Range<u128>,
) -> Result<Vec<Bookmark>> {
self.database.transaction(|tx| {
let mut stmt = list_by_project_id_range_stmt(tx)
.context("Failed to prepare list_by_project_id statement")?;
let mut rows = stmt
.query(rusqlite::named_params! {
":project_id": project_id,
":start": range.start.to_string(),
":end": range.end.to_string(),
})
.context("Failed to execute list_by_project_id statement")?;
let mut bookmarks = Vec::new();
while let Some(row) = rows.next()? {
bookmarks.push(parse_row(row)?);
}
Ok(bookmarks)
})
}
fn list_by_project_id_all(&self, project_id: &str) -> Result<Vec<Bookmark>> {
self.database.transaction(|tx| {
let mut stmt = list_by_project_id_stmt(tx)
.context("Failed to prepare list_by_project_id statement")?;
let mut rows = stmt
.query(rusqlite::named_params! { ":project_id": project_id })
.context("Failed to execute list_by_project_id statement")?;
let mut bookmarks = Vec::new();
while let Some(row) = rows.next()? {
bookmarks.push(parse_row(row)?);
}
Ok(bookmarks)
})
}
pub fn list_by_project_id(
&self,
project_id: &str,
range: Option<ops::Range<u128>>,
) -> Result<Vec<Bookmark>> {
if range.is_some() {
self.list_by_project_id_range(project_id, range.unwrap())
} else {
self.list_by_project_id_all(project_id)
}
}
pub fn get_by_row_id(&self, rowid: &i64) -> Result<Option<Bookmark>> {
self.database.transaction(|tx| {
let mut stmt =
get_by_rowid_stmt(tx).context("Failed to prepare get_by_rowid statement")?;
let mut rows = stmt
.query(rusqlite::named_params! { ":rowid": rowid })
.context("Failed to execute get_by_rowid statement")?;
if let Some(row) = rows.next()? {
Ok(Some(parse_row(row)?))
} else {
Ok(None)
}
})
}
pub fn on<F>(&self, callback: F) -> Result<()>
where
F: Fn(&Bookmark) + Send + 'static,
{
let boxed_self = Box::new(self.clone());
self.database.on_update(
move |action, _database_name, table_name, rowid| match action {
rusqlite::hooks::Action::SQLITE_INSERT | rusqlite::hooks::Action::SQLITE_UPDATE => {
match table_name {
"bookmarks" => match boxed_self.get_by_row_id(&rowid) {
Ok(Some(bookmark)) => callback(&bookmark),
Ok(None) => {}
Err(e) => log::error!("Failed to get bookmark: {}", e),
},
_ => {}
}
}
_ => {}
},
)
}
}
fn insert_stmt<'conn>(
tx: &'conn rusqlite::Transaction,
) -> Result<rusqlite::CachedStatement<'conn>> {
Ok(tx.prepare_cached(
"
INSERT INTO `bookmarks` (`id`, `project_id`, `timestamp_ms`, `note`)
VALUES (:id, :project_id, :timestamp_ms, :note)
",
)?)
}
fn list_by_project_id_range_stmt<'conn>(
tx: &'conn rusqlite::Transaction,
) -> Result<rusqlite::CachedStatement<'conn>> {
Ok(tx.prepare_cached(
"
SELECT `id`, `project_id`, `timestamp_ms`, `note`
FROM `bookmarks`
WHERE `project_id` = :project_id
AND `timestamp_ms` >= :start
AND `timestamp_ms` < :end
ORDER BY `timestamp_ms` DESC
",
)?)
}
fn list_by_project_id_stmt<'conn>(
tx: &'conn rusqlite::Transaction,
) -> Result<rusqlite::CachedStatement<'conn>> {
Ok(tx.prepare_cached(
"
SELECT `id`, `project_id`, `timestamp_ms`, `note`
FROM `bookmarks`
WHERE `project_id` = :project_id
ORDER BY `timestamp_ms` DESC
",
)?)
}
fn get_by_rowid_stmt<'conn>(
tx: &'conn rusqlite::Transaction,
) -> Result<rusqlite::CachedStatement<'conn>> {
Ok(tx.prepare_cached(
"
SELECT `id`, `project_id`, `timestamp_ms`, `note`
FROM `bookmarks`
WHERE `rowid` = :rowid
",
)?)
}
fn parse_row(row: &rusqlite::Row) -> Result<Bookmark> {
Ok(Bookmark {
id: row.get(0).context("Failed to get id")?,
project_id: row.get(1).context("Failed to get project_id")?,
timestamp_ms: row
.get::<usize, String>(2)
.context("Failed to get timestamp_ms")?
.parse()
.context("Failed to parse timestamp_ms")?,
note: row.get(3).context("Failed to get note")?,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn get_by_rowid() -> Result<()> {
let db = database::Database::memory()?;
let database = Database::new(db);
let bookmark = Bookmark {
id: "id".to_string(),
project_id: "project_id".to_string(),
timestamp_ms: 123,
note: "note".to_string(),
};
database.insert(&bookmark)?;
let rowid = database
.database
.transaction(|tx| {
Ok(tx
.prepare_cached("SELECT rowid FROM bookmarks LIMIT 1")?
.query_row([], |row| row.get(0))?)
})
.context("Failed to get rowid")?;
let result = database.get_by_row_id(&rowid)?;
assert_eq!(result, Some(bookmark));
Ok(())
}
#[test]
fn list_by_project_id_all() -> Result<()> {
let db = database::Database::memory()?;
let database = Database::new(db);
let bookmark = Bookmark {
id: "id".to_string(),
project_id: "project_id".to_string(),
timestamp_ms: 123,
note: "note".to_string(),
};
database.insert(&bookmark)?;
let result = database.list_by_project_id_all(&bookmark.project_id)?;
assert_eq!(result, vec![bookmark]);
Ok(())
}
#[test]
fn list_by_project_id_range() -> Result<()> {
let db = database::Database::memory()?;
let database = Database::new(db);
let bookmark_one = Bookmark {
id: "id".to_string(),
project_id: "project_id".to_string(),
timestamp_ms: 123,
note: "note".to_string(),
};
database.insert(&bookmark_one)?;
let bookmark_two = Bookmark {
id: "id2".to_string(),
project_id: "project_id".to_string(),
timestamp_ms: 456,
note: "note".to_string(),
};
database.insert(&bookmark_two)?;
let result = database.list_by_project_id_range(
&bookmark_one.project_id,
ops::Range { start: 0, end: 250 },
)?;
assert_eq!(result, vec![bookmark_one]);
Ok(())
}
}

View File

@ -0,0 +1,14 @@
mod database;
use serde::Serialize;
#[derive(Debug, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Bookmark {
pub id: String,
pub project_id: String,
pub timestamp_ms: u128,
pub note: String,
}
pub use database::Database;

View File

@ -1,4 +1,5 @@
mod app;
pub mod bookmarks;
pub mod deltas;
pub mod files;
pub mod gb_repository;

View File

@ -0,0 +1,8 @@
CREATE TABLE `bookmarks` (
`id` text NOT NULL PRIMARY KEY,
`project_id` text NOT NULL,
`timestamp_ms` text NOT NULL,
`note` text NOT NULL
);
CREATE INDEX bookmarks_project_id_idx ON `bookmarks` (`project_id`);