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

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
target
result

208
Cargo.lock generated Normal file
View File

@ -0,0 +1,208 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "ahash"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [
"cfg-if",
"once_cell",
"version_check",
"zerocopy",
]
[[package]]
name = "allocator-api2"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
[[package]]
name = "bitflags"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
[[package]]
name = "calibre-db"
version = "0.1.0"
dependencies = [
"rusqlite",
"thiserror",
]
[[package]]
name = "cc"
version = "1.0.96"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "065a29261d53ba54260972629f9ca6bffa69bac13cd1fed61420f7fa68b9f8bd"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "fallible-iterator"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
[[package]]
name = "fallible-streaming-iterator"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
dependencies = [
"ahash",
"allocator-api2",
]
[[package]]
name = "hashlink"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "692eaaf7f7607518dd3cef090f1474b61edc5301d8012f09579920df68b725ee"
dependencies = [
"hashbrown",
]
[[package]]
name = "libsqlite3-sys"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f"
dependencies = [
"cc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "once_cell"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "pkg-config"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
[[package]]
name = "proc-macro2"
version = "1.0.81"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rusqlite"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae"
dependencies = [
"bitflags",
"fallible-iterator",
"fallible-streaming-iterator",
"hashlink",
"libsqlite3-sys",
"smallvec",
]
[[package]]
name = "smallvec"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
name = "syn"
version = "2.0.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "1.0.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version_check"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "zerocopy"
version = "0.7.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
dependencies = [
"proc-macro2",
"quote",
"syn",
]

5
Cargo.toml Normal file
View File

@ -0,0 +1,5 @@
[workspace]
resolver = "2"
members = [
"calibre-db",
]

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.

145
flake.lock Normal file
View File

@ -0,0 +1,145 @@
{
"nodes": {
"fenix": {
"inputs": {
"nixpkgs": "nixpkgs",
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1714544767,
"narHash": "sha256-kF1bX+YFMedf1g0PAJYwGUkzh22JmULtj8Rm4IXAQKs=",
"owner": "nix-community",
"repo": "fenix",
"rev": "73124e1356bde9411b163d636b39fe4804b7ca45",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "fenix",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"naersk": {
"inputs": {
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1713520724,
"narHash": "sha256-CO8MmVDmqZX2FovL75pu5BvwhW+Vugc7Q6ze7Hj8heI=",
"owner": "nix-community",
"repo": "naersk",
"rev": "c5037590290c6c7dae2e42e7da1e247e54ed2d49",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "naersk",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1714253743,
"narHash": "sha256-mdTQw2XlariysyScCv2tTE45QSU9v/ezLcHJ22f0Nxc=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "58a1abdbae3217ca6b702f03d3b35125d88a2994",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 0,
"narHash": "sha256-mdTQw2XlariysyScCv2tTE45QSU9v/ezLcHJ22f0Nxc=",
"path": "/nix/store/801l7gvdz7yaibhjsxqx82sc7zkakjbq-source",
"type": "path"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1714253743,
"narHash": "sha256-mdTQw2XlariysyScCv2tTE45QSU9v/ezLcHJ22f0Nxc=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "58a1abdbae3217ca6b702f03d3b35125d88a2994",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"fenix": "fenix",
"flake-utils": "flake-utils",
"naersk": "naersk",
"nixpkgs": "nixpkgs_3"
}
},
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1714501997,
"narHash": "sha256-g31zfxwUFzkPgX0Q8sZLcrqGmOxwjEZ/iqJjNx4fEGo=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "49e502b277a8126a9ad10c802d1aaa3ef1a280ef",
"type": "github"
},
"original": {
"owner": "rust-lang",
"ref": "nightly",
"repo": "rust-analyzer",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

148
flake.nix Normal file
View File

@ -0,0 +1,148 @@
{
description = "rusty-cops project";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
naersk.url = "github:nix-community/naersk";
fenix.url = "github:nix-community/fenix";
};
outputs =
{
self,
nixpkgs,
naersk,
fenix,
flake-utils,
...
}:
let
buildTargets = {
"x86_64-linux" = {
crossSystemConfig = "x86_64-unknown-linux-musl";
rustTarget = "x86_64-unknown-linux-musl";
};
"i686-linux" = {
crossSystemConfig = "i686-unknown-linux-musl";
rustTarget = "i686-unknown-linux-musl";
};
"aarch64-linux" = {
crossSystemConfig = "aarch64-unknown-linux-musl";
rustTarget = "aarch64-unknown-linux-musl";
};
"armv6l-linux" = {
crossSystemConfig = "armv6l-unknown-linux-musleabihf";
rustTarget = "arm-unknown-linux-musleabihf";
};
};
eachSystem =
supportedSystems: callback:
builtins.foldl' (overall: system: overall // { ${system} = callback system; }) { } supportedSystems;
eachCrossSystem =
supportedSystems: callback:
eachSystem supportedSystems (
buildSystem:
builtins.foldl' (
inner: targetSystem: inner // { "cross-${targetSystem}" = callback buildSystem targetSystem; }
) { default = callback buildSystem buildSystem; } supportedSystems
);
mkPkgs =
buildSystem: targetSystem:
import nixpkgs (
{
system = buildSystem;
}
// (
if targetSystem == null then
{ }
else
{ crossSystem.config = buildTargets.${targetSystem}.crossSystemConfig; }
)
);
in
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = import nixpkgs { inherit system; };
rust = fenix.packages.${system}.stable;
in
with pkgs;
{
devShells.default = mkShell {
buildInputs = [
mosquitto
rust.toolchain
rust-analyzer
sea-orm-cli
];
};
}
)
// {
packages = eachCrossSystem (builtins.attrNames buildTargets) (
buildSystem: targetSystem:
let
pkgs = mkPkgs buildSystem null;
pkgsCross = mkPkgs buildSystem targetSystem;
rustTarget = buildTargets.${targetSystem}.rustTarget;
fenixPkgs = fenix.packages.${buildSystem};
mkToolchain = fenixPkgs: fenixPkgs.stable;
toolchain = fenixPkgs.combine [
(mkToolchain fenixPkgs).rustc
(mkToolchain fenixPkgs).cargo
(mkToolchain fenixPkgs.targets.${rustTarget}).rust-std
];
buildPackageAttrs =
if builtins.hasAttr "makeBuildPackageAttrs" buildTargets.${targetSystem} then
buildTargets.${targetSystem}.makeBuildPackageAttrs pkgsCross
else
{ };
naersk-lib = pkgs.callPackage naersk {
cargo = toolchain;
rustc = toolchain;
};
in
naersk-lib.buildPackage (
buildPackageAttrs
// rec {
src = ./.;
strictDeps = true;
doCheck = false;
OPENSSL_STATIC = "1";
OPENSSL_LIB_DIR = "${pkgsCross.pkgsStatic.openssl.out}/lib";
OPENSSL_INCLUDE_DIR = "${pkgsCross.pkgsStatic.openssl.dev}/include";
# Required because ring crate is special. This also seems to have
# fixed some issues with the x86_64-windows cross-compile :shrug:
TARGET_CC = "${pkgsCross.stdenv.cc}/bin/${pkgsCross.stdenv.cc.targetPrefix}cc";
CARGO_BUILD_TARGET = rustTarget;
CARGO_BUILD_RUSTFLAGS = [
"-C"
"target-feature=+crt-static"
# -latomic is required to build openssl-sys for armv6l-linux, but
# it doesn't seem to hurt any other builds.
# "-C"
# "link-args=-static -latomic"
"-C"
"linker=${TARGET_CC}"
];
}
)
);
};
}