- Fix all 96 doctest failures across 20 files by adding hidden imports and proper test setup (68 pass, 33 intentionally ignored) - Fix set_item_tags: wrap in transaction and replace item.id.unwrap() with proper error handling - Fix get_items_matching: replace N+1 per-item meta queries with batch get_meta_for_items() call - Fix get_item_matching: apply meta filtering instead of ignoring the parameter - Remove duplicate doc comment in store_meta - Remove dead code files: plugin.rs, plugins.rs, binary_detection.rs (never declared as modules) - Apply cargo fmt formatting fixes - Add keep.db to .gitignore
1446 lines
38 KiB
Rust
1446 lines
38 KiB
Rust
use anyhow::{Context, Error, Result, anyhow};
|
|
use chrono::prelude::*;
|
|
use lazy_static::lazy_static;
|
|
use log::*;
|
|
use rusqlite::{Connection, OpenFlags, params};
|
|
use rusqlite_migration::{M, Migrations};
|
|
use serde::{Deserialize, Serialize};
|
|
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:
|
|
```ignore
|
|
let conn = db::open(PathBuf::from("keep.db"))?;
|
|
```
|
|
Insert an item:
|
|
```ignore
|
|
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
|
|
///
|
|
/// ```
|
|
/// # use keep::db;
|
|
/// # use keep::db::*;
|
|
/// # use std::path::PathBuf;
|
|
/// # fn main() -> anyhow::Result<()> {
|
|
/// let db_path = PathBuf::from("keep.db");
|
|
/// let conn = db::open(db_path)?;
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
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
|
|
///
|
|
/// ```
|
|
/// # use keep::db;
|
|
/// # use keep::db::*;
|
|
/// # use chrono::Utc;
|
|
/// # use std::path::PathBuf;
|
|
/// # fn main() -> anyhow::Result<()> {
|
|
/// let db_path = PathBuf::from("keep.db");
|
|
/// let conn = db::open(db_path)?;
|
|
/// let item = Item {
|
|
/// id: None,
|
|
/// ts: Utc::now(),
|
|
/// size: None,
|
|
/// compression: "lz4".to_string(),
|
|
/// };
|
|
/// let id = db::insert_item(&conn, item)?;
|
|
/// assert!(id > 0);
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
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
|
|
///
|
|
/// ```
|
|
/// # use keep::db;
|
|
/// # use keep::db::*;
|
|
/// # use keep::compression_engine::CompressionType;
|
|
/// # use std::path::PathBuf;
|
|
/// # fn main() -> anyhow::Result<()> {
|
|
/// let db_path = PathBuf::from("keep.db");
|
|
/// let conn = db::open(db_path)?;
|
|
/// let compression = CompressionType::LZ4;
|
|
/// let item = db::create_item(&conn, compression)?;
|
|
/// assert!(item.id.is_some());
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
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
|
|
///
|
|
/// ```
|
|
/// # use keep::db;
|
|
/// # use keep::db::*;
|
|
/// # use chrono::Utc;
|
|
/// # use std::path::PathBuf;
|
|
/// # fn main() -> anyhow::Result<()> {
|
|
/// let db_path = PathBuf::from("keep.db");
|
|
/// let conn = db::open(db_path)?;
|
|
/// let item = Item { id: None, ts: Utc::now(), size: None, compression: "lz4".to_string() };
|
|
/// let item_id = db::insert_item(&conn, item)?;
|
|
/// db::add_tag(&conn, item_id, "important")?;
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
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
|
|
///
|
|
/// ```
|
|
/// # use keep::db;
|
|
/// # use keep::db::*;
|
|
/// # use chrono::Utc;
|
|
/// # use std::path::PathBuf;
|
|
/// # fn main() -> anyhow::Result<()> {
|
|
/// let db_path = PathBuf::from("keep.db");
|
|
/// let conn = db::open(db_path)?;
|
|
/// let item = Item { id: None, ts: Utc::now(), size: None, compression: "lz4".to_string() };
|
|
/// let item_id = db::insert_item(&conn, item)?;
|
|
/// db::add_meta(&conn, item_id, "mime_type", "text/plain")?;
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
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
|
|
///
|
|
/// ```
|
|
/// # use keep::db;
|
|
/// # use keep::db::*;
|
|
/// # use chrono::Utc;
|
|
/// # use std::path::PathBuf;
|
|
/// # fn main() -> anyhow::Result<()> {
|
|
/// let db_path = PathBuf::from("keep.db");
|
|
/// let conn = db::open(db_path)?;
|
|
/// let item = Item { id: Some(1), size: Some(1024), compression: "lz4".to_string(), ts: Utc::now() };
|
|
/// db::update_item(&conn, item)?;
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
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
|
|
///
|
|
/// ```
|
|
/// # use keep::db;
|
|
/// # use keep::db::*;
|
|
/// # use chrono::Utc;
|
|
/// # use std::path::PathBuf;
|
|
/// # fn main() -> anyhow::Result<()> {
|
|
/// let db_path = PathBuf::from("keep.db");
|
|
/// let conn = db::open(db_path)?;
|
|
/// let item = Item { id: Some(1), ts: Utc::now(), size: None, compression: "lz4".to_string() };
|
|
/// db::delete_item(&conn, item)?;
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
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
|
|
///
|
|
/// ```
|
|
/// # use keep::db;
|
|
/// # use keep::db::*;
|
|
/// # use std::path::PathBuf;
|
|
/// # fn main() -> anyhow::Result<()> {
|
|
/// let db_path = PathBuf::from("keep.db");
|
|
/// let conn = db::open(db_path)?;
|
|
/// let meta = Meta { id: 1, name: "temp".to_string(), value: "".to_string() };
|
|
/// db::query_delete_meta(&conn, meta)?;
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
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
|
|
///
|
|
/// ```
|
|
/// # use keep::db;
|
|
/// # use keep::db::*;
|
|
/// # use chrono::Utc;
|
|
/// # use std::path::PathBuf;
|
|
/// # fn main() -> anyhow::Result<()> {
|
|
/// let db_path = PathBuf::from("keep.db");
|
|
/// let conn = db::open(db_path)?;
|
|
/// let item = Item { id: None, ts: Utc::now(), size: None, compression: "lz4".to_string() };
|
|
/// let item_id = db::insert_item(&conn, item)?;
|
|
/// let meta = Meta { id: item_id, name: "mime_type".to_string(), value: "text/plain".to_string() };
|
|
/// db::query_upsert_meta(&conn, meta)?;
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
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
|
|
///
|
|
/// ```
|
|
/// # use keep::db;
|
|
/// # use keep::db::*;
|
|
/// # use chrono::Utc;
|
|
/// # use std::path::PathBuf;
|
|
/// # fn main() -> anyhow::Result<()> {
|
|
/// let db_path = PathBuf::from("keep.db");
|
|
/// let conn = db::open(db_path)?;
|
|
/// let item = Item { id: None, ts: Utc::now(), size: None, compression: "lz4".to_string() };
|
|
/// let item_id = db::insert_item(&conn, item)?;
|
|
/// // Insert new metadata
|
|
/// let meta = Meta { id: item_id, name: "source".to_string(), value: "cli".to_string() };
|
|
/// db::store_meta(&conn, meta)?;
|
|
///
|
|
/// // Delete metadata with empty value
|
|
/// let meta = Meta { id: item_id, name: "temp".to_string(), value: "".to_string() };
|
|
/// db::store_meta(&conn, meta)?;
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
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
|
|
///
|
|
/// ```
|
|
/// # use keep::db;
|
|
/// # use keep::db::*;
|
|
/// # use chrono::Utc;
|
|
/// # use std::path::PathBuf;
|
|
/// # fn main() -> anyhow::Result<()> {
|
|
/// let db_path = PathBuf::from("keep.db");
|
|
/// let conn = db::open(db_path)?;
|
|
/// let item = Item { id: None, ts: Utc::now(), size: None, compression: "lz4".to_string() };
|
|
/// let item_id = db::insert_item(&conn, item)?;
|
|
/// let tag = Tag { id: item_id, name: "work".to_string() };
|
|
/// db::insert_tag(&conn, tag)?;
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
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
|
|
///
|
|
/// ```
|
|
/// # use keep::db;
|
|
/// # use keep::db::*;
|
|
/// # use chrono::Utc;
|
|
/// # use std::path::PathBuf;
|
|
/// # fn main() -> anyhow::Result<()> {
|
|
/// let db_path = PathBuf::from("keep.db");
|
|
/// let conn = db::open(db_path)?;
|
|
/// let item = Item { id: Some(1), ts: Utc::now(), size: None, compression: "lz4".to_string() };
|
|
/// db::delete_item_tags(&conn, item)?;
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
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
|
|
///
|
|
/// ```
|
|
/// # use keep::db;
|
|
/// # use keep::db::*;
|
|
/// # use chrono::Utc;
|
|
/// # use std::path::PathBuf;
|
|
/// # fn main() -> anyhow::Result<()> {
|
|
/// let db_path = PathBuf::from("keep.db");
|
|
/// let conn = db::open(db_path)?;
|
|
/// let item = Item { id: None, ts: Utc::now(), size: None, compression: "lz4".to_string() };
|
|
/// let item_id = db::insert_item(&conn, item)?;
|
|
/// let item = Item { id: Some(item_id), ts: Utc::now(), size: None, compression: "lz4".to_string() };
|
|
/// let tags = vec!["project_a".to_string(), "urgent".to_string()];
|
|
/// db::set_item_tags(&conn, item, &tags)?;
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
pub fn set_item_tags(conn: &Connection, item: Item, tags: &Vec<String>) -> Result<()> {
|
|
debug!("DB: Setting tags for item: {item:?} ?{tags:?}");
|
|
let item_id = item
|
|
.id
|
|
.ok_or_else(|| anyhow!("Item ID is required for set_item_tags"))?;
|
|
let tx = conn.unchecked_transaction()?;
|
|
delete_item_tags(&tx, item)?;
|
|
for tag_name in tags {
|
|
insert_tag(
|
|
&tx,
|
|
Tag {
|
|
id: item_id,
|
|
name: tag_name.to_string(),
|
|
},
|
|
)?;
|
|
}
|
|
tx.commit()?;
|
|
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
|
|
///
|
|
/// ```
|
|
/// # use keep::db;
|
|
/// # use keep::db::*;
|
|
/// # use std::path::PathBuf;
|
|
/// # fn main() -> anyhow::Result<()> {
|
|
/// let db_path = PathBuf::from("keep.db");
|
|
/// let conn = db::open(db_path)?;
|
|
/// let all_items = db::query_all_items(&conn)?;
|
|
/// assert!(all_items.len() >= 0);
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
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
|
|
///
|
|
/// ```
|
|
/// # use keep::db;
|
|
/// # use keep::db::*;
|
|
/// # use std::path::PathBuf;
|
|
/// # fn main() -> anyhow::Result<()> {
|
|
/// let db_path = PathBuf::from("keep.db");
|
|
/// let conn = db::open(db_path)?;
|
|
/// let tags = vec!["work".to_string(), "urgent".to_string()];
|
|
/// let tagged_items = db::query_tagged_items(&conn, &tags)?;
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
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
|
|
///
|
|
/// ```
|
|
/// # use keep::db;
|
|
/// # use keep::db::*;
|
|
/// # use std::path::PathBuf;
|
|
/// # fn main() -> anyhow::Result<()> {
|
|
/// let db_path = PathBuf::from("keep.db");
|
|
/// let conn = db::open(db_path)?;
|
|
/// let items = db::get_items(&conn)?;
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
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
|
|
///
|
|
/// ```
|
|
/// # use keep::db;
|
|
/// # use keep::db::*;
|
|
/// # use std::collections::HashMap;
|
|
/// # use std::path::PathBuf;
|
|
/// # fn main() -> anyhow::Result<()> {
|
|
/// let db_path = PathBuf::from("keep.db");
|
|
/// let conn = db::open(db_path)?;
|
|
/// 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)?;
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
pub fn get_items_matching(
|
|
conn: &Connection,
|
|
tags: &Vec<String>,
|
|
meta: &HashMap<String, String>,
|
|
) -> Result<Vec<Item>> {
|
|
debug!("DB: Getting items matching: tags={tags:?} meta={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 item_ids: Vec<i64> = items.iter().filter_map(|i| i.id).collect();
|
|
let meta_map = get_meta_for_items(conn, &item_ids)?;
|
|
let filtered_items: Vec<Item> = items
|
|
.into_iter()
|
|
.filter(|item| {
|
|
let item_id = match item.id {
|
|
Some(id) => id,
|
|
None => return false,
|
|
};
|
|
let item_meta = match meta_map.get(&item_id) {
|
|
Some(m) => m,
|
|
None => return false,
|
|
};
|
|
meta.iter().all(|(k, v)| item_meta.get(k) == Some(v))
|
|
})
|
|
.collect();
|
|
Ok(filtered_items)
|
|
}
|
|
}
|
|
|
|
/// Gets a single item matching specified tags and metadata.
|
|
///
|
|
/// Returns the most recent item matching all tags and metadata.
|
|
///
|
|
/// # 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<Option<Item>>` - The matching item or None if not found, or an error.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// * Query execution errors.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// # use keep::db;
|
|
/// # use keep::db::*;
|
|
/// # use std::collections::HashMap;
|
|
/// # use std::path::PathBuf;
|
|
/// # fn main() -> anyhow::Result<()> {
|
|
/// let db_path = PathBuf::from("keep.db");
|
|
/// let conn = db::open(db_path)?;
|
|
/// let tags = vec!["latest".to_string()];
|
|
/// let item = db::get_item_matching(&conn, &tags, &HashMap::new())?;
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
pub fn get_item_matching(
|
|
conn: &Connection,
|
|
tags: &Vec<String>,
|
|
meta: &HashMap<String, String>,
|
|
) -> Result<Option<Item>> {
|
|
debug!("DB: Get item matching tags: {tags:?}, meta: {meta:?}");
|
|
let items = get_items_matching(conn, tags, meta)?;
|
|
Ok(items.into_iter().last())
|
|
}
|
|
|
|
/// 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
|
|
///
|
|
/// ```
|
|
/// # use keep::db;
|
|
/// # use keep::db::*;
|
|
/// # use chrono::Utc;
|
|
/// # use std::path::PathBuf;
|
|
/// # fn main() -> anyhow::Result<()> {
|
|
/// let db_path = PathBuf::from("keep.db");
|
|
/// let conn = db::open(db_path)?;
|
|
/// let item = Item { id: None, ts: Utc::now(), size: None, compression: "lz4".to_string() };
|
|
/// let item_id = db::insert_item(&conn, item)?;
|
|
/// let item = db::get_item(&conn, item_id)?;
|
|
/// assert!(item.is_some());
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
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
|
|
///
|
|
/// ```
|
|
/// # use keep::db;
|
|
/// # use keep::db::*;
|
|
/// # use std::path::PathBuf;
|
|
/// # fn main() -> anyhow::Result<()> {
|
|
/// let db_path = PathBuf::from("keep.db");
|
|
/// let conn = db::open(db_path)?;
|
|
/// let latest = db::get_item_last(&conn)?;
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
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
|
|
///
|
|
/// ```
|
|
/// # use keep::db;
|
|
/// # use keep::db::*;
|
|
/// # use chrono::Utc;
|
|
/// # use std::path::PathBuf;
|
|
/// # fn main() -> anyhow::Result<()> {
|
|
/// let db_path = PathBuf::from("keep.db");
|
|
/// let conn = db::open(db_path)?;
|
|
/// let item = Item { id: Some(1), ts: Utc::now(), size: None, compression: "lz4".to_string() };
|
|
/// let tags = db::get_item_tags(&conn, &item)?;
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
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
|
|
///
|
|
/// ```
|
|
/// # use keep::db;
|
|
/// # use keep::db::*;
|
|
/// # use chrono::Utc;
|
|
/// # use std::path::PathBuf;
|
|
/// # fn main() -> anyhow::Result<()> {
|
|
/// let db_path = PathBuf::from("keep.db");
|
|
/// let conn = db::open(db_path)?;
|
|
/// let item = Item { id: Some(1), ts: Utc::now(), size: None, compression: "lz4".to_string() };
|
|
/// let meta = db::get_item_meta(&conn, &item)?;
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
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
|
|
///
|
|
/// ```
|
|
/// # use keep::db;
|
|
/// # use keep::db::*;
|
|
/// # use chrono::Utc;
|
|
/// # use std::path::PathBuf;
|
|
/// # fn main() -> anyhow::Result<()> {
|
|
/// let db_path = PathBuf::from("keep.db");
|
|
/// let conn = db::open(db_path)?;
|
|
/// let item = Item { id: Some(1), ts: Utc::now(), size: None, compression: "lz4".to_string() };
|
|
/// let meta = db::get_item_meta_name(&conn, &item, "mime_type".to_string())?;
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
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
|
|
///
|
|
/// ```
|
|
/// # use keep::db;
|
|
/// # use keep::db::*;
|
|
/// # use chrono::Utc;
|
|
/// # use std::path::PathBuf;
|
|
/// # fn main() -> anyhow::Result<()> {
|
|
/// let db_path = PathBuf::from("keep.db");
|
|
/// let conn = db::open(db_path)?;
|
|
/// let item = Item { id: Some(1), ts: Utc::now(), size: None, compression: "lz4".to_string() };
|
|
/// let value = db::get_item_meta_value(&conn, &item, "source".to_string())?;
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
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
|
|
///
|
|
/// ```
|
|
/// # use keep::db;
|
|
/// # use keep::db::*;
|
|
/// # use std::path::PathBuf;
|
|
/// # fn main() -> anyhow::Result<()> {
|
|
/// let db_path = PathBuf::from("keep.db");
|
|
/// let conn = db::open(db_path)?;
|
|
/// let ids = vec![1, 2, 3];
|
|
/// let tags_map = db::get_tags_for_items(&conn, &ids)?;
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
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 ({placeholders_str}) ORDER BY id ASC, name ASC"
|
|
);
|
|
|
|
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_default().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
|
|
///
|
|
/// ```
|
|
/// # use keep::db;
|
|
/// # use keep::db::*;
|
|
/// # use std::path::PathBuf;
|
|
/// # fn main() -> anyhow::Result<()> {
|
|
/// let db_path = PathBuf::from("keep.db");
|
|
/// let conn = db::open(db_path)?;
|
|
/// let ids = vec![1, 2, 3];
|
|
/// let meta_map = db::get_meta_for_items(&conn, &ids)?;
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
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 ({placeholders_str}) ORDER BY id ASC, name ASC"
|
|
);
|
|
|
|
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_default().insert(name, value);
|
|
}
|
|
|
|
Ok(meta_map)
|
|
}
|