Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@ When changing rust files, make sure to run `cargo fmt` to format the code and

Next to this, ensure the code compiles and passes all tests by running:

- `cargo check`
- `cargo test`
- `cargo check --workspace` (or `cargo check` from workspace root)
- `cargo test --workspace` (or `cargo test` from workspace root)

Or use:

- `devenv shell -- cargo check`
- `devenv shell -- cargo test`
- `devenv shell -- cargo check --workspace`
- `devenv shell -- cargo test --workspace`

**Note:** The project uses a Rust workspace with members `src-tauri` and `src-tauri/calibre_db`. Run commands from the workspace root (`/workspaces/kikou`) to check and test all crates together, or use `--workspace` flag to ensure both crates are included.

## Frontend/Typescript Files

Expand Down Expand Up @@ -60,9 +62,14 @@ Tests should be added first before the actual implementation.

Rust tests can be executed via:

`cargo test --manifest-path=src-tauri/Cargo.toml`
`cargo test --workspace`
Or use:
`devenv shell -- cargo test --manifest-path=src-tauri/Cargo.toml`
`devenv shell -- cargo test --workspace`

To run tests for a specific crate:

- `cargo test -p calibre_db` (calibre_db crate only)
- `cargo test --manifest-path=src-tauri/Cargo.toml` (src-tauri crate only)

## Code Style

Expand Down
106 changes: 106 additions & 0 deletions .github/instructions/calibre_db.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
---
applyTo: "src-tauri/calibre_db/**/*.rs"
---

# calibre_db Crate - Calibre SQLite Database Bindings

This is a standalone, publishable Rust crate providing type-safe access to Calibre SQLite databases based on Calibre's database schema (from `third_party/calibre/src/calibre/db`).

## Purpose

Read and deserialize book metadata from Calibre's SQLite `metadata.db` format, supporting 10 core tables (books, authors, publishers, tags, series, data, comments, ratings, identifiers, languages) and their many-to-many relationships.

## Key Modules

- `lib.rs`: Public API (`CalibreDatabase::open()`, `get_book()`, `all_books()`)
- `models.rs`: Book, Author, Series, Tag, Identifier data structures with builder patterns
- `schema.rs`: Database connection management and schema validation
- `queries.rs`: Query functions for books and related metadata
- `error.rs`: Error types (DatabaseError, NotFound, InvalidData, IoError, SerializationError)

## Development Guidelines

- Query functions return `Result<T>` or `Result<Option<T>>` for missing relations
- Use builder pattern (`with_authors()`, `with_tags()`, etc.) for complex object construction
- Handle missing relations gracefully (some books lack series, publishers, ratings)
- Always verify book IDs before querying relations in loops
- Parse timestamps using RFC 3339, fallback to Utc::now()
- Fetch complete book data in single operation for performance

## Database Schema Notes

### Languages Table

The `books_languages_link` table uses a foreign key relationship to the `languages` table:

```sql
CREATE TABLE languages (
id INTEGER PRIMARY KEY,
lang_code TEXT NOT NULL
);

CREATE TABLE books_languages_link (
id INTEGER PRIMARY KEY,
book INTEGER NOT NULL,
lang_code INTEGER NOT NULL,
item_order INTEGER NOT NULL DEFAULT 0,
UNIQUE(book, lang_code)
);
```

Language codes are stored as references (foreign keys) to the `languages` table, not as direct text columns. When querying languages:

- Join `books_languages_link` with `languages` to fetch language codes
- Use `ORDER BY item_order ASC` to maintain language order
- Handle cases where books have no associated languages

## Testing

- Unit tests in each module (models, error, schema, queries)
- Integration tests in `tests/integration_tests.rs` with real SQLite databases
- All tests must create temporary databases; no file I/O to real libraries
- Verify both presence and absence of optional relations

## Build & Test

```
cargo check -p calibre_db # Verify compilation
cargo fmt # Format code (or cargo fmt -p calibre_db)
cargo clippy -p calibre_db # Lint (or run from calibre_db directory)
cargo test -p calibre_db # Run all tests
cargo test -p calibre_db --lib # Unit tests only
cargo test -p calibre_db --test integration_tests # Integration tests
```

Alternatively, run from the workspace root or use `--workspace` to check all crates:

```
cargo check --workspace
cargo test --workspace
cargo clippy --workspace
```

## Troubleshooting

### Schema Verification

If you encounter unexpected column errors or schema-related issues, ask the user for their Calibre database path, then verify the actual database schema with:

```bash
sqlite3 <USER_PROVIDED_DB_PATH> ".schema TABLE_NAME"
```

Replace `<USER_PROVIDED_DB_PATH>` with the user's actual Calibre `metadata.db` file path and `TABLE_NAME` with the table being investigated (e.g., `books_languages_link`, `books`, `authors`).

### Common Issues

**"no such column" errors**: The database schema may differ from expectations. Request the user's database path and use the schema verification command above to inspect the actual column names and structure.

**Language code retrieval failures**: Calibre uses a foreign key relationship between `books_languages_link` and `languages` tables. Ask the user for their database path and verify both table schemas:

```bash
sqlite3 <USER_PROVIDED_DB_PATH> ".schema books_languages_link"
sqlite3 <USER_PROVIDED_DB_PATH> ".schema languages"
```

Ensure queries properly join these tables and handle cases where books have no associated languages.
95 changes: 95 additions & 0 deletions .github/instructions/error.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
---
applyTo: "src-tauri/**/*.rs"
---

# Error Handling in Rust

Consistent error handling across all Rust modules using enums for type safety and clarity.

## Purpose

Provide a unified error handling strategy that:

- Prioritizes enum error types for domain-specific errors
- Ensures Tauri commands return the top-level `AppError` type for IPC communication
- Uses specific error enums for internal library functions
- Maintains clear error boundaries between modules

## Error Hierarchy

### Tauri Commands Layer

Tauri commands should always return `Result<T, AppError>` to ensure proper serialization for IPC:

```rust
#[tauri::command]
async fn fetch_book(lib: State<Arc<dyn BookLibrary>>, id: u32) -> Result<Book, AppError> {
lib.get_book(id).await
}
```

### Internal Library Layer

Internal functions should use specific error enums for their domain:

```rust
pub enum LibraryError {
BookNotFound(u32),
InvalidPath(String),
DatabaseFailure(String),
}

pub fn get_book_internal(id: u32) -> Result<Book, LibraryError> {
// Implementation
}
```

### AppError - Top-Level Error Type

The unified error enum for converting all domain-specific errors to IPC-compatible format.

**Variants:**

- `LibraryError(String)`: General library operation failures (path validation, initialization)
- `BookNotFound(String)`: Specific book not found by ID or query
- `DatabaseError(String)`: Underlying SQLite or database failures
- `IoError(String)`: File system errors
- `ValidationError(String)`: Invalid input or data validation issues
- `ArchiveError(String)`: Archive/compression operation failures

## Error Conversion

Implement `From` traits to convert domain-specific errors to `AppError`:

```rust
impl From<LibraryError> for AppError {
fn from(err: LibraryError) -> Self {
match err {
LibraryError::BookNotFound(id) => AppError::BookNotFound(format!("Book {} not found", id)),
LibraryError::InvalidPath(p) => AppError::ValidationError(format!("Invalid path: {}", p)),
LibraryError::DatabaseFailure(e) => AppError::DatabaseError(e),
}
}
}

impl From<std::io::Error> for AppError {
fn from(err: std::io::Error) -> Self {
AppError::IoError(err.to_string())
}
}

impl From<serde_json::Error> for AppError {
fn from(err: serde_json::Error) -> Self {
AppError::ValidationError(err.to_string())
}
}
```

## Guidelines

- **Prioritize enums**: Use specific error enums for internal operations (e.g., `LibraryError`, `ArchiveError`)
- **Tauri boundary**: Always return `AppError` from Tauri commands
- **Error conversion**: Implement `From` traits to bridge domain-specific errors to `AppError`
- **Descriptive messages**: Include context in error messages for debugging
- **No panics**: Return errors instead of panicking in library code
- **Result types**: Use `Result<T, E>` consistently throughout the codebase
90 changes: 90 additions & 0 deletions .github/instructions/library.calibre.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
---
applyTo: "src-tauri/src/library/calibre.rs"
---

# Why `unsafe impl Send + Sync` is Safe and Necessary

## The Problem

`CalibreLibrary` wraps `Arc<CalibreDatabase>`, which internally contains `rusqlite::Connection`. While `rusqlite::Connection` is thread-safe, Rust's type system cannot automatically verify this because it uses `RefCell` internally for statement caching.

The compiler requires `Send + Sync` for any type used across async task boundaries, but cannot derive these traits automatically from `Connection`.

## The Solution: `unsafe impl`

```rust
// SAFETY: CalibreDatabase wraps rusqlite::Connection, which is thread-safe.
// rusqlite uses SQLite's built-in locking mechanisms to ensure safe concurrent access.
// All database operations in this crate are read-only, preventing data races.
// The Arc<CalibreDatabase> pattern safely shares the connection across async tasks.
unsafe impl Send for CalibreLibrary {}
unsafe impl Sync for CalibreLibrary {}
```

## Why This Is Safe

### 1. rusqlite::Connection is Thread-Safe

- rusqlite wraps SQLite's C library, which provides thread-safe database access
- SQLite uses internal mutexes for serialization
- The Rust wrapper correctly exposes these guarantees
- See: https://www.sqlite.org/threadsafe.html

### 2. All Operations Are Read-Only

- The calibre_db crate only queries the database
- No mutations occur
- No shared mutable state across threads
- Data races are impossible with read-only access

### 3. Arc Provides Safe Sharing

- `Arc<T>` is always `Send + Sync` if `T` is `Send + Sync`
- Atomic reference counting handles concurrent access safely
- The Arc itself is allocated on the heap with stable address
- No use-after-free or double-free can occur

### 4. Compiler Verification Still Applies

While marked `unsafe impl`, the actual usage is verified as safe:

- Closures capturing `self` cannot violate Send/Sync requirements
- The async runtime cannot move tasks between threads unsafely
- Tauri's event loop enforces single-threaded execution where needed

## Why Not Other Approaches?

### Approach: tokio::task::spawn_blocking

- Would require copying all data into blocking tasks
- Unnecessary performance overhead for read-only operations
- Adds complexity without benefit

### Approach: Don't make it async

- Violates the trait definition which requires async
- Cannot integrate properly with Tauri's async runtime
- Blocks on database I/O instead of yielding

### Approach: RwLock or Mutex wrapper

- rusqlite::Connection already handles internal locking
- Adding another lock layer creates unnecessary contention
- Defeats the purpose of SQLite's built-in concurrency

## When This Pattern Is Appropriate

Use `unsafe impl Send + Sync` when:

1. Wrapping a known thread-safe C/FFI library (like rusqlite)
2. All operations are read-only or the library handles synchronization
3. The safety invariants are clearly documented
4. The usage is verified as safe through design

This is a common and accepted pattern in Rust for FFI bindings.

## References

- Rust nomicon on FFI and thread safety: https://doc.rust-lang.org/nomicon/ffi.html
- rusqlite documentation: https://docs.rs/rusqlite/
- Tokio task blocking: https://tokio.rs/tokio/tutorial/select#cancellation
Loading
Loading