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, /// Timestamp when the item was created. pub ts: DateTime, /// Size of the item content in bytes, None if not set. pub size: Option, /// 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` - 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 { 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` - 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 { 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` - 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 { 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) -> 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>` - 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> { 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>` - 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) -> Result> { 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 = 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>` - All items or an error. /// /// # Examples /// /// ``` /// let items = db::get_items(&conn)?; /// ``` pub fn get_items(conn: &Connection) -> Result> { 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>` - 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, meta: &HashMap, ) -> Result> { 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 = Vec::new(); for item in items.iter() { let mut item_ok = true; let mut item_meta: HashMap = 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>` - 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, _meta: &HashMap, ) -> Result> { 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 = 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>` - 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> { 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>` - 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> { 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>` - 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> { 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>` - 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> { 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>` - 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> { 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>` - 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> { 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>>` - 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>> { 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 = 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> = 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>>` - 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>> { 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 = 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> = 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) }