Library Database
The library subsystem stores all book metadata, reading progress, and table-of-contents data in SQLite. This page explains the schema, the key database types, and how data flows from disk into the database.
Schema overview
The database is created and versioned by the SQL migration files in
crates/core/migrations/. The initial schema defines eleven tables plus one
aggregating view:
erDiagram
books {
TEXT fingerprint PK
TEXT title
TEXT file_path
TEXT file_kind
INTEGER file_size
INTEGER added_at
}
authors {
INTEGER id PK
TEXT name
}
book_authors {
TEXT book_fingerprint FK
INTEGER author_id FK
INTEGER position
}
categories {
INTEGER id PK
TEXT name
}
book_categories {
TEXT book_fingerprint FK
INTEGER category_id FK
}
reading_states {
TEXT fingerprint PK
INTEGER opened
INTEGER current_page
INTEGER pages_count
INTEGER finished
}
thumbnails {
TEXT fingerprint PK
BLOB thumbnail_data
}
toc_entries {
TEXT id PK
TEXT book_fingerprint FK
TEXT parent_id FK
INTEGER position
TEXT title
TEXT location_kind
}
libraries {
INTEGER id PK
TEXT path
TEXT name
INTEGER created_at
}
library_books {
INTEGER library_id FK
TEXT book_fingerprint FK
INTEGER added_to_library_at
}
_cadmus_migrations {
TEXT id PK
INTEGER executed_at
TEXT status
}
books ||--o{ book_authors : ""
authors ||--o{ book_authors : ""
books ||--o{ book_categories : ""
categories ||--o{ book_categories : ""
books ||--o| reading_states : ""
books ||--o| thumbnails : ""
books ||--o{ toc_entries : ""
toc_entries ||--o{ toc_entries : "parent_id"
libraries ||--o{ library_books : ""
books ||--o{ library_books : ""
Key design choices
booksis the main table. Every other per-book table referencesbooks.fingerprintwithON DELETE CASCADE, so deleting a book row removes all associated data automatically.- Authors are normalised.
authorsholds unique author names;book_authorsis the join table and carries apositioncolumn that preserves display order. - All tables use
STRICTmode. SQLite’sSTRICTpragma enforces column type constraints at the storage layer, catching type mismatches early. - Timestamps are Unix epoch integers.
added_at,created_at, and similar columns areINTEGER NOT NULL; neverTEXT. - TOC tree via adjacency list.
toc_entries.parent_idis a self-reference;positionpreserves sibling order. Theidis a UUID7 (generated in Rust) soORDER BY id ASCgives stable insertion order without a growing rowid. library_books_full_infoview. An aggregating view joinsbooks,reading_states,book_authors,authors,book_categories, andcategoriesin one query. Thelibrary_idcolumn fromlibrary_booksis exposed so callers can filter with a plainWHERE library_id = ?.
Data access layer
The
cadmus_core::library::db::Db
struct is the entry point for all library database operations. It wraps the
shared SqlitePool and exposes a synchronous API by bridging every async
SQLx call through the global Tokio runtime:
flowchart LR
caller["Caller (sync event loop)"]
Db["library::db::Db"]
RUNTIME["RUNTIME.block_on()"]
SQLx["SQLx async query"]
SQLite[("SQLite")]
caller -->|sync call| Db
Db -->|wraps in| RUNTIME
RUNTIME -->|awaits| SQLx
SQLx -->|reads/writes| SQLite
SQLx -->|result| RUNTIME
RUNTIME -->|returns| caller
The sync bridge exists because Cadmus’s UI event loop is single-threaded and
synchronous. The global RUNTIME (a tokio::runtime::Runtime singleton) lets
the rest of the codebase call database methods without needing to be async.
Key methods on Db:
| Method | Purpose |
|---|---|
register_library | Insert a new library row and return its id |
get_library_by_path | Look up a library id by filesystem path |
get_all_books | Fetch every book in a library via the full-info view |
insert_book | Write a new book and its authors/categories |
save_reading_state | Save or update reading progress for a book |
save_toc | Bulk-write a book’s table of contents |
get_thumbnail | Retrieve the stored cover thumbnail BLOB |
save_thumbnail | Save or replace a cover thumbnail |
How a book scan flows into the database
When a library directory is scanned, Cadmus follows this sequence:
sequenceDiagram
participant Scanner as Library Scanner
participant Db as library::db::Db
participant SQLite
Scanner->>Db: register_library(path, name)
Db->>SQLite: INSERT INTO libraries
SQLite-->>Db: library_id
loop for each book file
Scanner->>Db: insert_book(library_id, fp, info)
Db->>SQLite: INSERT INTO books
Db->>SQLite: INSERT INTO authors / book_authors
Db->>SQLite: INSERT INTO book_categories
Db->>SQLite: INSERT INTO library_books
end
loop for each book with reading progress
Scanner->>Db: save_reading_state(fp, reader_info)
Db->>SQLite: INSERT OR REPLACE INTO reading_states
end
loop for each book with a TOC
Scanner->>Db: save_toc(fp, entries)
Db->>SQLite: INSERT INTO toc_entries
end
Related pages
- SQLite & SQLx — compile-time query verification, review rules
- Runtime Migrations — one-time data migrations using
the
migration!macro