bare bones calibre db reading

This commit is contained in:
Sebastian Hugentobler 2024-05-01 16:21:07 +02:00
commit 65e17fc55b
Signed by: shu
GPG key ID: BB32CF3CA052C2F0
13 changed files with 790 additions and 0 deletions

8
calibre-db/Cargo.toml Normal file
View file

@ -0,0 +1,8 @@
[package]
name = "calibre-db"
version = "0.1.0"
edition = "2021"
[dependencies]
rusqlite = { version = "0.31.0", features = ["bundled"] }
thiserror = "1.0.59"

94
calibre-db/src/calibre.rs Normal file
View file

@ -0,0 +1,94 @@
use rusqlite::Connection;
use crate::data::{author::Author, book::Book, error::DataStoreError, pagination::SortOrder};
pub struct Calibre {
conn: Connection,
}
impl Calibre {
pub fn load(url: &str) -> Result<Self, DataStoreError> {
let conn = Connection::open(url)?;
Ok(Self { conn })
}
pub fn books(
&self,
limit: u64,
cursor: Option<&str>,
sort_order: SortOrder,
) -> Result<Vec<Book>, DataStoreError> {
Book::books(&self.conn, limit, cursor, sort_order)
}
pub fn authors(
&self,
limit: u64,
cursor: Option<&str>,
sort_order: SortOrder,
) -> Result<Vec<Author>, DataStoreError> {
Author::authors(&self.conn, limit, cursor, sort_order)
}
pub fn author_books(
&self,
author_id: u64,
limit: u64,
cursor: Option<&str>,
sort_order: SortOrder,
) -> Result<Vec<Book>, DataStoreError> {
Book::author_books(&self.conn, author_id, limit, cursor, sort_order)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn books() {
let c = Calibre::load("./testdata/metadata.db").unwrap();
let books = c.books(10, None, SortOrder::ASC).unwrap();
assert_eq!(books.len(), 4);
}
#[test]
fn authors() {
let c = Calibre::load("./testdata/metadata.db").unwrap();
let authors = c.authors(10, None, SortOrder::ASC).unwrap();
assert_eq!(authors.len(), 3);
}
#[test]
fn author_books() {
let c = Calibre::load("./testdata/metadata.db").unwrap();
let books = c.author_books(1, 10, None, SortOrder::ASC).unwrap();
assert_eq!(books.len(), 2);
}
#[test]
fn pagination() {
let c = Calibre::load("./testdata/metadata.db").unwrap();
let authors = c.authors(1, None, SortOrder::ASC).unwrap();
assert_eq!(authors.len(), 1);
assert_eq!(authors[0].name, "Kevin R. Grazier");
let authors = c
.authors(1, Some(&authors[0].sort), SortOrder::ASC)
.unwrap();
assert_eq!(authors.len(), 1);
assert_eq!(authors[0].name, "Terry Pratchett");
let authors = c
.authors(1, Some(&authors[0].sort), SortOrder::ASC)
.unwrap();
assert_eq!(authors.len(), 1);
assert_eq!(authors[0].name, "Edward Noyes Westcott");
let authors = c
.authors(1, Some(&authors[0].sort), SortOrder::DESC)
.unwrap();
assert_eq!(authors.len(), 1);
assert_eq!(authors[0].name, "Terry Pratchett");
}
}

View file

@ -0,0 +1,38 @@
use rusqlite::{Connection, Row};
use super::{
error::DataStoreError,
pagination::{Pagination, SortOrder},
};
#[derive(Debug)]
pub struct Author {
pub id: i32,
pub name: String,
pub sort: String,
}
impl Author {
fn from_row(row: &Row<'_>) -> Result<Self, rusqlite::Error> {
Ok(Self {
id: row.get(0)?,
name: row.get(1)?,
sort: row.get(2)?,
})
}
pub fn authors(
conn: &Connection,
limit: u64,
cursor: Option<&str>,
sort_order: SortOrder,
) -> Result<Vec<Author>, DataStoreError> {
let pagination = Pagination::new("sort", cursor, limit, sort_order);
pagination.paginate(
conn,
"SELECT id, name, sort FROM authors",
&[],
Self::from_row,
)
}
}

View file

@ -0,0 +1,56 @@
use rusqlite::{Connection, Row};
use super::{
error::DataStoreError,
pagination::{Pagination, SortOrder},
};
#[derive(Debug)]
pub struct Book {
pub id: i32,
pub title: String,
pub sort: String,
}
impl Book {
fn from_row(row: &Row<'_>) -> Result<Self, rusqlite::Error> {
Ok(Self {
id: row.get(0)?,
title: row.get(1)?,
sort: row.get(2)?,
})
}
pub fn books(
conn: &Connection,
limit: u64,
cursor: Option<&str>,
sort_order: SortOrder,
) -> Result<Vec<Book>, DataStoreError> {
let pagination = Pagination::new("sort", cursor, limit, sort_order);
pagination.paginate(
conn,
"SELECT id, title, sort FROM books",
&[],
Self::from_row,
)
}
pub fn author_books(
conn: &Connection,
author_id: u64,
limit: u64,
cursor: Option<&str>,
sort_order: SortOrder,
) -> Result<Vec<Book>, DataStoreError> {
let pagination = Pagination::new("books.sort", cursor, limit, sort_order);
pagination.paginate(
conn,
"SELECT books.id, books.title, books.sort FROM books \
INNER JOIN books_authors_link ON books.id = books_authors_link.book \
WHERE books_authors_link.author = (:author_id) AND",
&[(":author_id", &author_id)],
Self::from_row,
)
}
}

View file

@ -0,0 +1,7 @@
use thiserror::Error;
#[derive(Error, Debug)]
pub enum DataStoreError {
#[error("sqlite error")]
SqliteError(#[from] rusqlite::Error),
}

View file

@ -0,0 +1,72 @@
use rusqlite::{Connection, Row, ToSql};
use super::error::DataStoreError;
#[derive(Debug, PartialEq)]
pub enum SortOrder {
ASC,
DESC,
}
pub struct Pagination<'a> {
pub sort_col: &'a str,
pub limit: u64,
pub cursor: Option<&'a str>,
pub sort_order: SortOrder,
}
impl<'a> Pagination<'a> {
pub fn new(
sort_col: &'a str,
cursor: Option<&'a str>,
limit: u64,
sort_order: SortOrder,
) -> Self {
Self {
sort_col,
limit,
cursor,
sort_order,
}
}
pub fn paginate<T, F>(
&self,
conn: &Connection,
statement: &str,
params: &[(&str, &dyn ToSql)],
processor: F,
) -> Result<Vec<T>, DataStoreError>
where
F: FnMut(&Row<'_>) -> Result<T, rusqlite::Error>,
{
let cursor = self.cursor.unwrap_or("");
let comparison = if self.sort_order == SortOrder::ASC {
">"
} else {
"<"
};
let where_sql = if statement.ends_with("AND") {
""
} else {
"WHERE"
};
let sort_col = self.sort_col;
let sort_order = &self.sort_order;
// DANGER: vulnerable to SQL injection if statement or sort_col variable is influenced by user input
let mut stmt = conn.prepare(&format!(
"{statement} {where_sql} {sort_col} {comparison} (:cursor) ORDER BY {sort_col} {sort_order:?} LIMIT (:limit)"
))?;
let params = [
&[
(":cursor", &cursor as &dyn ToSql),
(":limit", &self.limit as &dyn ToSql),
],
params,
]
.concat();
let iter = stmt.query_map(params.as_slice(), processor)?;
Ok(iter.filter_map(Result::ok).collect())
}
}

7
calibre-db/src/lib.rs Normal file
View file

@ -0,0 +1,7 @@
pub mod calibre;
pub mod data {
pub mod author;
pub mod book;
pub mod error;
pub mod pagination;
}

BIN
calibre-db/testdata/metadata.db vendored Normal file

Binary file not shown.