DatabaseManager is the low-level persistence layer for Bibliotheca. It wraps a
SQLite connection, owns prepared statements, and provides a narrow set of CRUD
methods that the higher-level BookList uses to keep the database in sync.
sqlite3_open / sqlite3_close).ensure_schema).Book records.DatabaseManager
├── std::string db_path_ // path to the .sqlite file
├── sqlite3* db_ // raw SQLite handle (owned)
└── std::mutex mtx_ // serialises public API calls
All statements are prepared lazily by prepare_statements() and reused for each
operation.
ensure_schema() creates the following tables:
CREATE TABLE IF NOT EXISTS books (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
author TEXT NOT NULL DEFAULT '',
file_path TEXT NOT NULL,
cover_path TEXT NOT NULL DEFAULT '',
size_bytes INTEGER NOT NULL DEFAULT 0,
mtime_unix INTEGER NOT NULL DEFAULT 0,
added_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))
);
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL DEFAULT ''
);
CREATE TABLE IF NOT EXISTS tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE
);
CREATE TABLE IF NOT EXISTS book_tags (
book_id TEXT NOT NULL,
tag_id INTEGER NOT NULL,
FOREIGN KEY (book_id) REFERENCES books(id) ON DELETE CASCADE,
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE,
PRIMARY KEY (book_id, tag_id)
);
Additional indices or columns should be added here, keeping in mind that the manager is the canonical place to evolve storage.
| Method | Description |
|---|---|
ensure_schema() |
Creates tables if they do not exist. |
upsert_book(book) |
INSERT OR REPLACE using the book's id. |
remove_book(id) |
Deletes the row; returns false if not present. |
get_book(id) |
Loads a single book (optional result). |
load_all_books() |
Returns a vector of all books with their tags. |
| Method | Description |
|---|---|
add_tag_to_book(book_id, tag) |
Associates a tag with a book. |
remove_tag_from_book(book_id, tag) |
Removes a tag association from a book. |
get_tags_for_book(book_id) |
Returns all tags for a specific book. |
get_all_tags() |
Returns all tags with usage counts. |
rename_tag(old, new) |
Renames a tag (merges if target exists). |
delete_tag(tag_name) |
Deletes a tag from all books. |
delete_unused_tags() |
Removes orphan tags with no associations. |
| Method | Description |
|---|---|
get_setting(key) |
Get a string setting value. |
get_setting(key, default) |
Get setting with fallback value. |
set_setting(key, value) |
Store a string setting. |
get_setting_int(key, default) |
Get an integer setting. |
set_setting_int(key, value) |
Store an integer setting. |
| Method | Description |
|---|---|
get_all_book_files() |
Returns book file info for scanning. |
remove_books_by_ids(ids) |
Batch remove books by ID. |
Internally each method locks mtx_, executes the prepared statement, and
translates SQLite rows into Book objects. The add_tag_to_book method
creates the tag if it doesn't exist (INSERT OR IGNORE).
SQLite is file-based and can momentarily return SQLITE_BUSY if another thread
or process holds a lock. The static busy_handler callback retries with an
exponential backoff. Because all public methods take the mutex, only one thread
issues SQL commands at a time, simplifying concurrency.
ensure_schema() and keep
the Book serialization/deserialization in sync.BookList untouched.BookList owns a single DatabaseManager instance, calls ensure_schema() on
startup, and uses the CRUD helpers whenever a book changes. Other modules should
not talk to SQLite directly; instead they should route all persistence through
this manager to keep logic centralised.