Files
keep/src/db.rs
Andrew Phillips 9e11756d4a docs: Fully document db module functions with examples and details
Co-authored-by: aider (openai/andrew/openrouter/sonoma-sky-alpha) <aider@aider.chat>
2025-09-10 16:05:20 -03:00

1266 lines
33 KiB
Rust

use anyhow::{Context, Error, Result};
use chrono::prelude::*;
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
use log::*;
use rusqlite::{Connection, OpenFlags, params};
use rusqlite_migration::{M, Migrations};
use std::collections::HashMap;
use std::path::PathBuf;
use std::rc::Rc;
/// Database module for the Keep application.
///
/// This module provides SQLite database operations for storing and retrieving
/// items, tags, and metadata. It includes schema migrations, CRUD operations,
/// and query utilities for efficient data access.
///
/// # Schema
///
/// The database uses three main tables:
/// - `items`: Core item information (ID, timestamp, size, compression).
/// - `tags`: Item-tag associations (many-to-many).
/// - `metas`: Item-metadata associations (many-to-many).
///
/// Foreign keys are enforced with cascading deletes. Indexes on tag and meta
/// names improve query performance.
///
/// # Migrations
///
/// Automatic schema migrations are applied on database open using
/// `rusqlite_migration`. The current schema includes:
/// - Items table with auto-increment ID.
/// - Tags and metas tables with composite primary keys.
/// - Indexes on tag and meta names.
///
/// # Usage
///
/// Open a connection:
/// ```
/// let conn = db::open(PathBuf::from("keep.db"))?;
/// ```
/// Insert an item:
/// ```
/// let item = db::Item { id: None, ts: Utc::now(), size: None, compression: "lz4".to_string() };
/// let id = db::insert_item(&conn, item)?;
/// ```
lazy_static! {
/// Database schema migrations for the Keep application.
///
/// Defines the sequence of migrations to create and update the schema.
/// Applied automatically when opening a database connection.
static ref MIGRATIONS: Migrations<'static> = Migrations::new(vec![
M::up(
"CREATE TABLE items(
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
ts TEXT NOT NULL,
size INTEGER NULL,
compression TEXT NOT NULL)"
),
M::up(
"CREATE TABLE tags (
id INTEGER NOT NULL,
name TEXT NOT NULL,
FOREIGN KEY(id) REFERENCES items(id) ON DELETE CASCADE,
PRIMARY KEY(id, name));"
),
M::up(
"CREATE TABLE metas (
id INTEGER NOT NULL,
name TEXT NOT NULL,
value TEXT NOT NULL,
FOREIGN KEY(id) REFERENCES items(id) ON DELETE CASCADE,
PRIMARY KEY(id, name));"
),
M::up("CREATE INDEX idx_tags_name ON tags(name)"),
M::up("CREATE INDEX idx_metas_name ON metas(name)"),
]);
}
/// Represents an item stored in the database.
///
/// Core structure for items containing basic properties.
///
/// # Fields
///
/// * `id` - Unique identifier, `None` for new items.
/// * `ts` - Creation timestamp in UTC.
/// * `size` - Content size in bytes, `None` if not set.
/// * `compression` - Compression algorithm used.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Item {
/// Unique identifier for the item, None for new items.
pub id: Option<i64>,
/// Timestamp when the item was created.
pub ts: DateTime<Utc>,
/// Size of the item content in bytes, None if not set.
pub size: Option<i64>,
/// Compression algorithm used for the item content.
pub compression: String,
}
/// Represents a tag associated with an item.
///
/// Defines the relationship between items and tags in a many-to-many structure.
///
/// # Fields
///
/// * `id` - Item ID this tag belongs to.
/// * `name` - Tag name (unique per item).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Tag {
/// ID of the item this tag belongs to.
pub id: i64,
/// Name of the tag.
pub name: String,
}
/// Represents metadata associated with an item.
///
/// Stores key-value metadata for items in a many-to-many relationship.
///
/// # Fields
///
/// * `id` - Item ID this metadata belongs to.
/// * `name` - Metadata field name (unique per item).
/// * `value` - Metadata field value.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Meta {
/// ID of the item this metadata belongs to.
pub id: i64,
/// Name of the metadata field.
pub name: String,
/// Value of the metadata field.
pub value: String,
}
/// Opens a database connection and ensures the schema is up to date.
///
/// Creates or opens the SQLite database file and applies any pending migrations.
/// Enables foreign keys and the array virtual table for advanced queries.
///
/// # Arguments
///
/// * `path` - Path to the SQLite database file.
///
/// # Returns
///
/// * `Result<Connection, Error>` - A SQLite connection on success, or an error if opening or migration fails.
///
/// # Errors
///
/// * Database file access issues.
/// * Migration failures.
/// * SQLite configuration errors.
///
/// # Examples
///
/// ```
/// let db_path = PathBuf::from("keep.db");
/// let conn = db::open(db_path)?;
/// ```
pub fn open(path: PathBuf) -> Result<Connection, Error> {
debug!("DB: Opening file: {:?}", path);
let mut conn = Connection::open_with_flags(
path,
OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE,
)
.context("Problem opening file")?;
conn.pragma_update(None, "foreign_keys", "ON")
.context("Problem enabling SQLite foreign_keys pragma")?;
MIGRATIONS
.to_latest(&mut conn)
.context("Problem performing database migrations")?;
rusqlite::vtab::array::load_module(&conn).context("Problem enabling array module")?;
Ok(conn)
}
/// Inserts a new item into the database.
///
/// Creates a new row in the `items` table and returns the generated ID.
///
/// # Arguments
///
/// * `conn` - Database connection.
/// * `item` - Item to insert (ID will be set automatically).
///
/// # Returns
///
/// * `Result<i64>` - The ID of the newly inserted item on success, or an error if insertion fails.
///
/// # Errors
///
/// * Database constraint violations.
/// * Connection issues.
///
/// # Examples
///
/// ```
/// let item = Item {
/// id: None,
/// ts: Utc::now(),
/// size: None,
/// compression: "lz4".to_string(),
/// };
/// let id = db::insert_item(&conn, item)?;
/// assert!(id > 0);
/// ```
pub fn insert_item(conn: &Connection, item: Item) -> Result<i64> {
debug!("DB: Inserting item: {:?}", item);
conn.execute(
"INSERT INTO items (ts, size, compression) VALUES (?1, ?2, ?3)",
params![item.ts, item.size, item.compression],
)?;
Ok(conn.last_insert_rowid())
}
/// Creates a new item in the database with the current timestamp.
///
/// Convenience function to create and insert a new item with default values.
///
/// # Arguments
///
/// * `conn` - Database connection.
/// * `compression_type` - Compression type to use for the item.
///
/// # Returns
///
/// * `Result<Item>` - The created item with its ID set, or an error if creation fails.
///
/// # Errors
///
/// * Database insertion errors.
///
/// # Examples
///
/// ```
/// let compression = CompressionType::LZ4;
/// let item = db::create_item(&conn, compression)?;
/// assert!(item.id.is_some());
/// ```
pub fn create_item(conn: &Connection, compression_type: crate::compression_engine::CompressionType) -> Result<Item> {
let item = Item {
id: None,
ts: chrono::Utc::now(),
size: None,
compression: compression_type.to_string(),
};
let item_id = insert_item(conn, item.clone())?;
Ok(Item {
id: Some(item_id),
..item
})
}
/// Adds a tag to an item.
///
/// Inserts a new tag association in the `tags` table.
///
/// # Arguments
///
/// * `conn` - Database connection.
/// * `item_id` - ID of the item to tag.
/// * `tag_name` - Name of the tag to add.
///
/// # Returns
///
/// * `Result<()>` - Success or error if the operation fails.
///
/// # Errors
///
/// * Duplicate tag constraint violation.
/// * Database errors.
///
/// # Examples
///
/// ```
/// db::add_tag(&conn, 1, "important")?;
/// ```
pub fn add_tag(conn: &Connection, item_id: i64, tag_name: &str) -> Result<()> {
let tag = Tag {
id: item_id,
name: tag_name.to_string(),
};
insert_tag(conn, tag)
}
/// Adds metadata to an item.
///
/// Inserts a new metadata entry in the `metas` table.
///
/// # Arguments
///
/// * `conn` - Database connection.
/// * `item_id` - ID of the item.
/// * `name` - Name of the metadata field.
/// * `value` - Value of the metadata field.
///
/// # Returns
///
/// * `Result<()>` - Success or error if the operation fails.
///
/// # Errors
///
/// * Duplicate metadata constraint violation.
/// * Database errors.
///
/// # Examples
///
/// ```
/// db::add_meta(&conn, 1, "mime_type", "text/plain")?;
/// ```
pub fn add_meta(conn: &Connection, item_id: i64, name: &str, value: &str) -> Result<()> {
let meta = Meta {
id: item_id,
name: name.to_string(),
value: value.to_string(),
};
store_meta(conn, meta)
}
/// Updates an existing item in the database.
///
/// Modifies size and compression fields for an existing item.
///
/// # Arguments
///
/// * `conn` - Database connection.
/// * `item` - Item with updated values (ID must be set).
///
/// # Returns
///
/// * `Result<()>` - Success or error if the operation fails.
///
/// # Errors
///
/// * Item not found.
/// * Database errors.
///
/// # Examples
///
/// ```
/// let item = Item { id: Some(1), size: Some(1024), compression: "lz4".to_string(), ts: Utc::now() };
/// db::update_item(&conn, item)?;
/// ```
pub fn update_item(conn: &Connection, item: Item) -> Result<()> {
debug!("DB: Updating item: {:?}", item);
conn.execute(
"UPDATE items SET size=?2, compression=?3 WHERE id=?1",
params![
item.id,
item.size,
item.compression,
],
)?;
Ok(())
}
/// Deletes an item from the database.
///
/// Removes the item and cascades to delete associated tags and metadata.
///
/// # Arguments
///
/// * `conn` - Database connection.
/// * `item` - Item to delete (ID must be set).
///
/// # Returns
///
/// * `Result<()>` - Success or error if the operation fails.
///
/// # Errors
///
/// * Item not found.
/// * Database errors.
///
/// # Examples
///
/// ```
/// let item = Item { id: Some(1), ..default_item() };
/// db::delete_item(&conn, item)?;
/// ```
pub fn delete_item(conn: &Connection, item: Item) -> Result<()> {
debug!("DB: Deleting item: {:?}", item);
conn.execute("DELETE FROM items WHERE id=?1", params![item.id])?;
Ok(())
}
/// Deletes a metadata entry from the database.
///
/// Removes a specific metadata field for an item.
///
/// # Arguments
///
/// * `conn` - Database connection.
/// * `meta` - Metadata entry to delete.
///
/// # Returns
///
/// * `Result<()>` - Success or error if the operation fails.
///
/// # Errors
///
/// * Metadata not found.
/// * Database errors.
///
/// # Examples
///
/// ```
/// let meta = Meta { id: 1, name: "temp".to_string(), value: "".to_string() };
/// db::query_delete_meta(&conn, meta)?;
/// ```
pub fn query_delete_meta(conn: &Connection, meta: Meta) -> Result<()> {
debug!("DB: Deleting meta: {:?}", meta);
conn.execute(
"DELETE FROM metas WHERE id=?1 AND name=?2",
params![meta.id, meta.name],
)?;
Ok(())
}
/// Inserts or updates a metadata entry in the database.
///
/// Uses UPSERT to handle both insert and update cases.
///
/// # Arguments
///
/// * `conn` - Database connection.
/// * `meta` - Metadata entry to upsert.
///
/// # Returns
///
/// * `Result<()>` - Success or error if the operation fails.
///
/// # Errors
///
/// * Database constraint violations.
/// * Connection issues.
///
/// # Examples
///
/// ```
/// let meta = Meta { id: 1, name: "mime_type".to_string(), value: "text/plain".to_string() };
/// db::query_upsert_meta(&conn, meta)?;
/// ```
pub fn query_upsert_meta(conn: &Connection, meta: Meta) -> Result<()> {
debug!("DB: Inserting meta: {:?}", meta);
conn.execute(
"INSERT INTO metas (id, name, value) VALUES (?1, ?2, ?3)
ON CONFLICT(id, name) DO UPDATE SET value=?3",
params![meta.id, meta.name, meta.value],
)?;
Ok(())
}
/// Stores a metadata entry, deleting it if the value is empty.
///
/// Handles both insertion/update and deletion based on value presence.
///
/// # Arguments
///
/// * `conn` - Database connection.
/// * `meta` - Metadata entry to store (empty value triggers deletion).
///
/// # Returns
///
/// * `Result<()>` - Success or error if the operation fails.
///
/// # Errors
///
/// * Database errors during insert/update/delete.
///
/// # Examples
///
/// ```
/// // Insert new metadata
/// let meta = Meta { id: 1, name: "source".to_string(), value: "cli".to_string() };
/// db::store_meta(&conn, meta)?;
///
/// // Delete metadata with empty value
/// let meta = Meta { id: 1, name: "temp".to_string(), value: "".to_string() };
/// db::store_meta(&conn, meta)?;
/// ```
/// Stores a metadata entry, deleting it if the value is empty.
///
/// Handles both insertion/update and deletion based on value presence.
///
/// # Arguments
///
/// * `conn` - Database connection.
/// * `meta` - Metadata entry to store (empty value triggers deletion).
///
/// # Returns
///
/// * `Result<()>` - Success or error if the operation fails.
///
/// # Errors
///
/// * Database errors during insert/update/delete.
///
/// # Examples
///
/// ```
/// // Insert new metadata
/// let meta = Meta { id: 1, name: "source".to_string(), value: "cli".to_string() };
/// db::store_meta(&conn, meta)?;
///
/// // Delete metadata with empty value
/// let meta = Meta { id: 1, name: "temp".to_string(), value: "".to_string() };
/// db::store_meta(&conn, meta)?;
/// ```
pub fn store_meta(conn: &Connection, meta: Meta) -> Result<()> {
if meta.value.is_empty() {
query_delete_meta(conn, meta)?;
} else {
query_upsert_meta(conn, meta)?;
}
Ok(())
}
/// Inserts a tag into the database.
///
/// Creates a new tag association (fails if duplicate).
///
/// # Arguments
///
/// * `conn` - Database connection.
/// * `tag` - Tag to insert.
///
/// # Returns
///
/// * `Result<()>` - Success or error if the operation fails.
///
/// # Errors
///
/// * Duplicate tag constraint violation.
/// * Database errors.
///
/// # Examples
///
/// ```
/// let tag = Tag { id: 1, name: "work".to_string() };
/// db::insert_tag(&conn, tag)?;
/// ```
pub fn insert_tag(conn: &Connection, tag: Tag) -> Result<()> {
debug!("DB: Inserting tag: {:?}", tag);
conn.execute(
"INSERT INTO tags (id, name) VALUES (?1, ?2)",
params![tag.id, tag.name],
)?;
Ok(())
}
/// Deletes all tags associated with an item.
///
/// Clears all tag associations for the specified item.
///
/// # Arguments
///
/// * `conn` - Database connection.
/// * `item` - Item whose tags should be deleted (ID must be set).
///
/// # Returns
///
/// * `Result<()>` - Success or error if the operation fails.
///
/// # Errors
///
/// * Database errors.
///
/// # Examples
///
/// ```
/// let item = Item { id: Some(1), .. };
/// db::delete_item_tags(&conn, item)?;
/// ```
pub fn delete_item_tags(conn: &Connection, item: Item) -> Result<()> {
debug!("DB: Deleting all item tags: {:?}", item);
conn.execute("DELETE FROM tags WHERE id=?1", params![item.id])?;
Ok(())
}
/// Sets the tags for an item, replacing any existing tags.
///
/// Deletes existing tags and inserts the new set.
///
/// # Arguments
///
/// * `conn` - Database connection.
/// * `item` - Item to set tags for (ID must be set).
/// * `tags` - Vector of tag names to set.
///
/// # Returns
///
/// * `Result<()>` - Success or error if the operation fails.
///
/// # Errors
///
/// * Database insertion errors.
/// * Item ID missing.
///
/// # Examples
///
/// ```
/// let item = Item { id: Some(1), .. };
/// let tags = vec!["project_a".to_string(), "urgent".to_string()];
/// db::set_item_tags(&conn, item, &tags)?;
/// ```
pub fn set_item_tags(conn: &Connection, item: Item, tags: &Vec<String>) -> Result<()> {
debug!("DB: Setting tags for item: {:?} ?{:?}", item, tags);
delete_item_tags(conn, item.clone())?;
let item_id = item.id.unwrap();
for tag_name in tags {
insert_tag(
conn,
Tag {
id: item_id,
name: tag_name.to_string(),
},
)?;
}
Ok(())
}
/// Queries all items from the database, ordered by ID.
///
/// Retrieves all items in ascending ID order.
///
/// # Arguments
///
/// * `conn` - Database connection.
///
/// # Returns
///
/// * `Result<Vec<Item>>` - All items in the database or an error.
///
/// # Errors
///
/// * Query execution errors.
///
/// # Examples
///
/// ```
/// let all_items = db::query_all_items(&conn)?;
/// assert!(all_items.len() >= 0);
/// ```
pub fn query_all_items(conn: &Connection) -> Result<Vec<Item>> {
debug!("DB: Querying all items");
let mut statement = conn
.prepare("SELECT id, ts, size, compression FROM items ORDER BY id ASC")
.context("Problem preparing SQL statement")?;
let mut rows = statement.query(params![])?;
let mut items = Vec::new();
while let Some(row) = rows.next()? {
let item = Item {
id: row.get(0)?,
ts: row.get(1)?,
size: row.get(2)?,
compression: row.get(3)?,
};
items.push(item);
}
Ok(items)
}
/// Queries items that have all the specified tags.
///
/// Uses array-based subquery to find items matching all tags.
///
/// # Arguments
///
/// * `conn` - Database connection.
/// * `tags` - Vector of tag names to match (all must match).
///
/// # Returns
///
/// * `Result<Vec<Item>>` - Matching items or an error.
///
/// # Errors
///
/// * Query preparation or execution errors.
///
/// # Examples
///
/// ```
/// let tags = vec!["work".to_string(), "urgent".to_string()];
/// let tagged_items = db::query_tagged_items(&conn, &tags)?;
/// ```
pub fn query_tagged_items<'a>(conn: &'a Connection, tags: &'a Vec<String>) -> Result<Vec<Item>> {
debug!("DB: Querying tagged items: {:?}", tags);
let mut statement = conn
.prepare_cached(
"
SELECT items.id,
items.ts,
items.size,
items.compression,
count(tags_match.id) as tags_score
FROM items,
(SELECT tags.id FROM tags WHERE tags.name IN rarray(?1)) as tags_match
WHERE items.id = tags_match.id
GROUP BY items.id
HAVING tags_score = ?2
ORDER BY items.id ASC",
)
.context("Problem preparing SQL statement")?;
let tags_values: Vec<rusqlite::types::Value> = tags
.iter()
.map(|s| rusqlite::types::Value::from(s.clone()))
.collect();
let tags_ptr = Rc::new(tags_values);
let mut rows = statement.query(params![&tags_ptr, &tags.len()])?;
let mut items = Vec::new();
while let Some(row) = rows.next()? {
let item = Item {
id: row.get(0)?,
ts: row.get(1)?,
size: row.get(2)?,
compression: row.get(3)?,
};
items.push(item);
}
Ok(items)
}
/// Gets all items from the database.
///
/// Alias for `query_all_items` for consistency.
///
/// # Arguments
///
/// * `conn` - Database connection.
///
/// # Returns
///
/// * `Result<Vec<Item>>` - All items or an error.
///
/// # Examples
///
/// ```
/// let items = db::get_items(&conn)?;
/// ```
pub fn get_items(conn: &Connection) -> Result<Vec<Item>> {
debug!("DB: Getting all items");
query_all_items(conn)
}
/// Gets items matching specified tags and metadata criteria.
///
/// First filters by tags (if provided), then applies metadata matching.
///
/// # Arguments
///
/// * `conn` - Database connection.
/// * `tags` - Vector of tag names to match (all must match).
/// * `meta` - HashMap of metadata key-value pairs to match (exact match).
///
/// # Returns
///
/// * `Result<Vec<Item>>` - Matching items or an error.
///
/// # Errors
///
/// * Query errors.
/// * Metadata query failures.
///
/// # Examples
///
/// ```
/// let tags = vec!["project".to_string()];
/// let meta = HashMap::from([("status".to_string(), "active".to_string())]);
/// let matching = db::get_items_matching(&conn, &tags, &meta)?;
/// ```
pub fn get_items_matching(
conn: &Connection,
tags: &Vec<String>,
meta: &HashMap<String, String>,
) -> Result<Vec<Item>> {
debug!(
"DB: Getting items matching: tags={:?} meta={:?}",
tags, meta
);
let items = match tags.is_empty() {
true => query_all_items(conn)?,
false => query_tagged_items(conn, tags)?,
};
if meta.is_empty() {
debug!("DB: Not filtering on meta");
Ok(items)
} else {
debug!("DB: Filtering on meta");
let mut filtered_items: Vec<Item> = Vec::new();
for item in items.iter() {
let mut item_ok = true;
let mut item_meta: HashMap<String, String> = HashMap::new();
for meta in get_item_meta(conn, item)? {
item_meta.insert(meta.name, meta.value);
}
debug!("DB: Matching: {:?}: {:?}", item, item_meta);
for (k, v) in meta.iter() {
match item_meta.get(k) {
Some(value) => item_ok = v.eq(value),
None => item_ok = false,
}
if !item_ok {
break;
}
}
if item_ok {
filtered_items.push(item.clone());
}
}
Ok(filtered_items)
}
}
/// Gets a single item matching specified tags.
///
/// Returns the most recent item matching all tags (ignores metadata).
///
/// # Arguments
///
/// * `conn` - Database connection.
/// * `tags` - Vector of tag names to match (all must match).
/// * `_meta` - Unused metadata parameter (for API consistency).
///
/// # Returns
///
/// * `Result<Option<Item>>` - The matching item or None if not found, or an error.
///
/// # Errors
///
/// * Query execution errors.
///
/// # Examples
///
/// ```
/// let tags = vec!["latest".to_string()];
/// let item = db::get_item_matching(&conn, &tags, &HashMap::new())?;
/// ```
pub fn get_item_matching(
conn: &Connection,
tags: &Vec<String>,
_meta: &HashMap<String, String>,
) -> Result<Option<Item>> {
debug!("DB: Get item matching tags: {:?}", tags);
let mut statement = conn
.prepare_cached(
"
SELECT items.id,
items.ts,
items.size,
items.compression,
count(sel.id) as score
FROM items,
(SELECT tags.id FROM tags WHERE tags.name IN rarray(?1)) as sel
WHERE items.id = sel.id
GROUP BY items.id
HAVING score = ?2
ORDER BY items.id DESC
LIMIT 1",
)
.context("Problem preparing SQL statement")?;
let tags_values: Vec<rusqlite::types::Value> = tags
.iter()
.map(|s| rusqlite::types::Value::from(s.clone()))
.collect();
let tags_ptr = Rc::new(tags_values);
let mut rows = statement.query(params![&tags_ptr, &tags.len()])?;
match rows.next()? {
Some(row) => Ok(Some(Item {
id: row.get(0)?,
ts: row.get(1)?,
size: row.get(2)?,
compression: row.get(3)?,
})),
None => Ok(None),
}
}
/// Gets an item by its ID.
///
/// Simple lookup by primary key.
///
/// # Arguments
///
/// * `conn` - Database connection.
/// * `item_id` - ID of the item to retrieve.
///
/// # Returns
///
/// * `Result<Option<Item>>` - The item if found, None if not found, or an error.
///
/// # Errors
///
/// * Query execution errors.
///
/// # Examples
///
/// ```
/// let item = db::get_item(&conn, 1)?;
/// assert!(item.is_some());
/// ```
pub fn get_item(conn: &Connection, item_id: i64) -> Result<Option<Item>> {
debug!("DB: Getting item {:?}", item_id);
let mut statement = conn
.prepare_cached(
"
SELECT id, ts, size, compression
FROM items
WHERE items.id = ?1",
)
.context("Problem preparing SQL statement")?;
let mut rows = statement.query(params![item_id])?;
match rows.next()? {
Some(row) => Ok(Some(Item {
id: row.get(0)?,
ts: row.get(1)?,
size: row.get(2)?,
compression: row.get(3)?,
})),
None => Ok(None),
}
}
/// Gets the most recently created item.
///
/// Retrieves the item with the highest ID (most recent).
///
/// # Arguments
///
/// * `conn` - Database connection.
///
/// # Returns
///
/// * `Result<Option<Item>>` - The most recent item or None if no items exist, or an error.
///
/// # Errors
///
/// * Query execution errors.
///
/// # Examples
///
/// ```
/// let latest = db::get_item_last(&conn)?;
/// ```
pub fn get_item_last(conn: &Connection) -> Result<Option<Item>> {
debug!("DB: Getting last item");
let mut statement = conn
.prepare_cached(
"
SELECT id, ts, size, compression
FROM items
ORDER BY id DESC
LIMIT 1",
)
.context("Problem preparing SQL statement")?;
let mut rows = statement.query(params![])?;
match rows.next()? {
Some(row) => Ok(Some(Item {
id: row.get(0)?,
ts: row.get(1)?,
size: row.get(2)?,
compression: row.get(3)?,
})),
None => Ok(None),
}
}
/// Gets all tags for a specific item.
///
/// Retrieves all tag associations ordered by name.
///
/// # Arguments
///
/// * `conn` - Database connection.
/// * `item` - Item to get tags for (ID must be set).
///
/// # Returns
///
/// * `Result<Vec<Tag>>` - The item's tags or an error.
///
/// # Errors
///
/// * Query execution errors.
///
/// # Examples
///
/// ```
/// let item = Item { id: Some(1), .. };
/// let tags = db::get_item_tags(&conn, &item)?;
/// ```
pub fn get_item_tags(conn: &Connection, item: &Item) -> Result<Vec<Tag>> {
debug!("DB: Getting tags for item: {:?}", item);
let mut statement = conn
.prepare_cached("SELECT id, name FROM tags WHERE id=?1 ORDER BY name ASC")
.context("Problem preparing SQL statement")?;
let mut rows = statement.query(params![item.id])?;
let mut tags = Vec::new();
while let Some(row) = rows.next()? {
tags.push(Tag {
id: row.get(0)?,
name: row.get(1)?,
});
}
Ok(tags)
}
/// Gets all metadata for a specific item.
///
/// Retrieves all metadata entries ordered by name.
///
/// # Arguments
///
/// * `conn` - Database connection.
/// * `item` - Item to get metadata for (ID must be set).
///
/// # Returns
///
/// * `Result<Vec<Meta>>` - The item's metadata or an error.
///
/// # Errors
///
/// * Query execution errors.
///
/// # Examples
///
/// ```
/// let item = Item { id: Some(1), .. };
/// let meta = db::get_item_meta(&conn, &item)?;
/// ```
pub fn get_item_meta(conn: &Connection, item: &Item) -> Result<Vec<Meta>> {
debug!("DB: Getting item meta: {:?}", item);
let mut statement = conn
.prepare_cached("SELECT id, name, value FROM metas WHERE id=?1 ORDER BY name ASC")
.context("Problem preparing SQL statement")?;
let mut rows = statement.query(params![item.id])?;
let mut metas = Vec::new();
while let Some(row) = rows.next()? {
metas.push(Meta {
id: row.get(0)?,
name: row.get(1)?,
value: row.get(2)?,
});
}
Ok(metas)
}
/// Gets a specific metadata entry for an item by name.
///
/// Retrieves a single metadata field by name.
///
/// # Arguments
///
/// * `conn` - Database connection.
/// * `item` - Item to get metadata for (ID must be set).
/// * `name` - Name of the metadata field.
///
/// # Returns
///
/// * `Result<Option<Meta>>` - The metadata entry if found, None if not found, or an error.
///
/// # Errors
///
/// * Query execution errors.
///
/// # Examples
///
/// ```
/// let item = Item { id: Some(1), .. };
/// let meta = db::get_item_meta_name(&conn, &item, "mime_type".to_string())?;
/// ```
pub fn get_item_meta_name(conn: &Connection, item: &Item, name: String) -> Result<Option<Meta>> {
debug!("DB: Getting item meta name: {:?} {:?}", item, name);
let mut statement = conn
.prepare_cached("SELECT id, name, value FROM metas WHERE id=?1 AND name=?2")
.context("Problem preparing SQL statement")?;
let mut rows = statement.query(params![item.id, name])?;
match rows.next()? {
Some(row) => Ok(Some(Meta {
id: row.get(0)?,
name: row.get(1)?,
value: row.get(2)?,
})),
None => Ok(None),
}
}
/// Gets the value of a specific metadata field for an item.
///
/// Retrieves just the value string for a metadata field.
///
/// # Arguments
///
/// * `conn` - Database connection.
/// * `item` - Item to get metadata for (ID must be set).
/// * `name` - Name of the metadata field.
///
/// # Returns
///
/// * `Result<Option<String>>` - The metadata value if found, None if not found, or an error.
///
/// # Errors
///
/// * Query execution errors.
///
/// # Examples
///
/// ```
/// let item = Item { id: Some(1), .. };
/// let value = db::get_item_meta_value(&conn, &item, "source".to_string())?;
/// ```
pub fn get_item_meta_value(conn: &Connection, item: &Item, name: String) -> Result<Option<String>> {
debug!("DB: Getting item meta value: {:?} {:?}", item, name);
let mut statement = conn
.prepare_cached("SELECT value FROM metas WHERE id=?1 AND name=?2")
.context("Problem preparing SQL statement")?;
let mut rows = statement.query(params![item.id, name])?;
match rows.next()? {
Some(row) => Ok(Some(row.get(0)?)),
None => Ok(None),
}
}
/// Gets tags for multiple items in a single query.
///
/// Efficiently retrieves tags for a batch of items using IN clause.
///
/// # Arguments
///
/// * `conn` - Database connection.
/// * `item_ids` - Slice of item IDs to get tags for.
///
/// # Returns
///
/// * `Result<HashMap<i64, Vec<Tag>>>` - Mapping of item IDs to their tags, or an error.
///
/// # Errors
///
/// * Query preparation or execution errors.
///
/// # Examples
///
/// ```
/// let ids = vec![1, 2, 3];
/// let tags_map = db::get_tags_for_items(&conn, &ids)?;
/// ```
pub fn get_tags_for_items(conn: &Connection, item_ids: &[i64]) -> Result<std::collections::HashMap<i64, Vec<Tag>>> {
debug!("DB: Getting tags for items: {:?}", item_ids);
if item_ids.is_empty() {
return Ok(std::collections::HashMap::new());
}
// Create placeholders for the IN clause
let placeholders: Vec<String> = item_ids.iter().map(|_| "?".to_string()).collect();
let placeholders_str = placeholders.join(",");
let sql = format!("SELECT id, name FROM tags WHERE id IN ({}) ORDER BY id ASC, name ASC", placeholders_str);
let mut statement = conn
.prepare_cached(&sql)
.context("Problem preparing SQL statement")?;
let mut rows = statement.query(rusqlite::params_from_iter(item_ids))?;
let mut tags_map: std::collections::HashMap<i64, Vec<Tag>> = std::collections::HashMap::new();
while let Some(row) = rows.next()? {
let id: i64 = row.get(0)?;
let name: String = row.get(1)?;
tags_map.entry(id).or_insert_with(Vec::new).push(Tag { id, name });
}
Ok(tags_map)
}
/// Gets metadata for multiple items in a single query.
///
/// Efficiently retrieves metadata for a batch of items using IN clause.
///
/// # Arguments
///
/// * `conn` - Database connection.
/// * `item_ids` - Slice of item IDs to get metadata for.
///
/// # Returns
///
/// * `Result<HashMap<i64, HashMap<String, String>>>` - Mapping of item IDs to their metadata, or an error.
///
/// # Errors
///
/// * Query preparation or execution errors.
///
/// # Examples
///
/// ```
/// let ids = vec![1, 2, 3];
/// let meta_map = db::get_meta_for_items(&conn, &ids)?;
/// ```
pub fn get_meta_for_items(conn: &Connection, item_ids: &[i64]) -> Result<std::collections::HashMap<i64, std::collections::HashMap<String, String>>> {
debug!("DB: Getting meta for items: {:?}", item_ids);
if item_ids.is_empty() {
return Ok(std::collections::HashMap::new());
}
// Create placeholders for the IN clause
let placeholders: Vec<String> = item_ids.iter().map(|_| "?".to_string()).collect();
let placeholders_str = placeholders.join(",");
let sql = format!("SELECT id, name, value FROM metas WHERE id IN ({}) ORDER BY id ASC, name ASC", placeholders_str);
let mut statement = conn
.prepare_cached(&sql)
.context("Problem preparing SQL statement")?;
let mut rows = statement.query(rusqlite::params_from_iter(item_ids))?;
let mut meta_map: std::collections::HashMap<i64, std::collections::HashMap<String, String>> = std::collections::HashMap::new();
while let Some(row) = rows.next()? {
let id: i64 = row.get(0)?;
let name: String = row.get(1)?;
let value: String = row.get(2)?;
meta_map.entry(id).or_insert_with(std::collections::HashMap::new).insert(name, value);
}
Ok(meta_map)
}