From b13d1350d6307d3761d3cf2b08f44aa2f9c76c5f Mon Sep 17 00:00:00 2001 From: Huakun Shen Date: Thu, 6 Mar 2025 15:51:21 -0500 Subject: [PATCH] feat(db): Add command aliases and usage tracking - Implement command_aliases table with FTS5 virtual table - Add usage_count and last_used_at columns to commands table - Create CRUD methods for command aliases - Add methods to track and retrieve command usage statistics - Update database migration system to support new schema version --- packages/db/sql/2025-03-04.sql | 44 +++++++++ packages/db/src/lib.rs | 173 ++++++++++++++++++++++++++++++++- packages/db/src/models.rs | 11 +++ packages/db/src/schema.rs | 9 +- 4 files changed, 231 insertions(+), 6 deletions(-) create mode 100644 packages/db/sql/2025-03-04.sql diff --git a/packages/db/sql/2025-03-04.sql b/packages/db/sql/2025-03-04.sql new file mode 100644 index 0000000..c58c817 --- /dev/null +++ b/packages/db/sql/2025-03-04.sql @@ -0,0 +1,44 @@ +CREATE TABLE IF NOT EXISTS command_aliases ( + alias_id INTEGER PRIMARY KEY AUTOINCREMENT, + cmd_id INTEGER NOT NULL, + alias TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(cmd_id, alias), + FOREIGN KEY (cmd_id) REFERENCES commands (cmd_id) ON DELETE CASCADE +); + +ALTER TABLE commands ADD COLUMN usage_count INTEGER DEFAULT 0; +ALTER TABLE commands ADD COLUMN last_used_at TIMESTAMP; + +-- Create FTS5 virtual table for command_aliases +CREATE VIRTUAL TABLE IF NOT EXISTS command_aliases_fts USING fts5( + alias_id UNINDEXED, + alias, + content = command_aliases, + content_rowid = alias_id, + tokenize = 'unicode61 remove_diacritics 2' +); + +-- Trigger to update FTS index when command_aliases is inserted +CREATE TRIGGER IF NOT EXISTS command_aliases_ai AFTER INSERT ON command_aliases BEGIN + INSERT INTO command_aliases_fts(alias_id, alias) + VALUES (new.alias_id, new.alias); +END; + +-- Trigger to update FTS index when command_aliases is updated +CREATE TRIGGER IF NOT EXISTS command_aliases_au AFTER UPDATE ON command_aliases BEGIN + INSERT INTO command_aliases_fts(command_aliases_fts, alias_id, alias) + VALUES ('delete', old.alias_id, old.alias); + + INSERT INTO command_aliases_fts(alias_id, alias) + VALUES (new.alias_id, new.alias); +END; + +-- Trigger to update FTS index when command_aliases is deleted +CREATE TRIGGER IF NOT EXISTS command_aliases_ad AFTER DELETE ON command_aliases BEGIN + INSERT INTO command_aliases_fts(command_aliases_fts, alias_id, alias) + VALUES ('delete', old.alias_id, old.alias); +END; + + diff --git a/packages/db/src/lib.rs b/packages/db/src/lib.rs index 5f4d27c..d3adb5f 100644 --- a/packages/db/src/lib.rs +++ b/packages/db/src/lib.rs @@ -5,7 +5,7 @@ use rusqlite::{params, params_from_iter, Connection, Result, ToSql}; use serde_json::{json, Value}; use std::path::Path; -pub const DB_VERSION: u32 = 1; +pub const DB_VERSION: u32 = 2; pub fn get_connection>( file_path: P, @@ -45,13 +45,14 @@ impl JarvisDB { Ok(()) } - pub fn migrate_after_version(&self, version: u16) -> Result<()> { + pub fn migrate_after_version(&self, mut version: u16) -> Result<()> { for migration in schema::MIGRATIONS.iter() { if migration.version > version { println!( "Migrating from version {} to {}", version, migration.version ); + version = migration.version; // self.conn.execute(&migration.schema, params![])?; match self .conn @@ -250,7 +251,7 @@ impl JarvisDB { pub fn get_command_by_id(&self, cmd_id: i32) -> Result> { let mut stmt = self .conn - .prepare("SELECT cmd_id, ext_id, name, type, data, alias, hotkey, enabled FROM commands WHERE cmd_id = ?1")?; + .prepare("SELECT cmd_id, ext_id, name, type, data, alias, hotkey, enabled, usage_count, last_used_at FROM commands WHERE cmd_id = ?1")?; let cmd_iter = stmt.query_map(params![cmd_id], |row| { Ok(models::Cmd { cmd_id: row.get(0)?, @@ -261,6 +262,8 @@ impl JarvisDB { alias: row.get(5)?, hotkey: row.get(6)?, enabled: row.get(7)?, + usage_count: row.get(8)?, + last_used_at: row.get(9)?, }) })?; let mut cmds = Vec::new(); @@ -273,7 +276,7 @@ impl JarvisDB { pub fn get_commands_by_ext_id(&self, ext_id: i32) -> Result> { let mut stmt = self .conn - .prepare("SELECT cmd_id, ext_id, name, type, data, alias, hotkey, enabled FROM commands WHERE ext_id = ?1")?; + .prepare("SELECT cmd_id, ext_id, name, type, data, alias, hotkey, enabled, usage_count, last_used_at FROM commands WHERE ext_id = ?1")?; let cmd_iter = stmt.query_map(params![ext_id], |row| { Ok(models::Cmd { cmd_id: row.get(0)?, @@ -284,6 +287,8 @@ impl JarvisDB { alias: row.get(5)?, hotkey: row.get(6)?, enabled: row.get(7)?, + usage_count: row.get(8)?, + last_used_at: row.get(9)?, }) })?; let mut cmds = Vec::new(); @@ -547,6 +552,108 @@ impl JarvisDB { )?; Ok(()) } + + /* -------------------------------------------------------------------------- */ + /* Command Aliases CRUD */ + /* -------------------------------------------------------------------------- */ + pub fn create_command_alias(&self, cmd_id: i32, alias: &str) -> Result<()> { + self.conn.execute( + "INSERT INTO command_aliases (cmd_id, alias) VALUES (?1, ?2)", + params![cmd_id, alias], + )?; + Ok(()) + } + + pub fn get_command_aliases_by_cmd_id(&self, cmd_id: i32) -> Result> { + let mut stmt = self.conn.prepare( + "SELECT alias_id, cmd_id, alias, created_at, updated_at FROM command_aliases WHERE cmd_id = ?1", + )?; + let alias_iter = stmt.query_map(params![cmd_id], |row| { + Ok(models::CmdAlias { + alias_id: row.get(0)?, + cmd_id: row.get(1)?, + alias: row.get(2)?, + created_at: row.get(3)?, + updated_at: row.get(4)?, + }) + })?; + let mut aliases = Vec::new(); + for alias in alias_iter { + aliases.push(alias?); + } + Ok(aliases) + } + + pub fn get_command_alias_by_id(&self, alias_id: i32) -> Result> { + let mut stmt = self.conn.prepare( + "SELECT alias_id, cmd_id, alias, created_at, updated_at FROM command_aliases WHERE alias_id = ?1", + )?; + let alias_iter = stmt.query_map(params![alias_id], |row| { + Ok(models::CmdAlias { + alias_id: row.get(0)?, + cmd_id: row.get(1)?, + alias: row.get(2)?, + created_at: row.get(3)?, + updated_at: row.get(4)?, + }) + })?; + let mut aliases = Vec::new(); + for alias in alias_iter { + aliases.push(alias?); + } + Ok(aliases.first().cloned()) + } + + pub fn delete_command_alias_by_id(&self, alias_id: i32) -> Result<()> { + self.conn.execute( + "DELETE FROM command_aliases WHERE alias_id = ?1", + params![alias_id], + )?; + Ok(()) + } + + pub fn search_command_aliases(&self, search_text: &str) -> Result> { + let mut stmt = self.conn.prepare( + "SELECT ca.alias_id, ca.cmd_id, ca.alias, ca.created_at, ca.updated_at + FROM command_aliases ca + JOIN command_aliases_fts caf ON ca.alias_id = caf.alias_id + WHERE caf.alias MATCH ?1", + )?; + let alias_iter = stmt.query_map(params![search_text], |row| { + Ok(models::CmdAlias { + alias_id: row.get(0)?, + cmd_id: row.get(1)?, + alias: row.get(2)?, + created_at: row.get(3)?, + updated_at: row.get(4)?, + }) + })?; + let mut aliases = Vec::new(); + for alias in alias_iter { + aliases.push(alias?); + } + Ok(aliases) + } + + pub fn update_command_usage(&self, cmd_id: i32) -> Result<()> { + self.conn.execute( + "UPDATE commands SET usage_count = usage_count + 1, last_used_at = CURRENT_TIMESTAMP WHERE cmd_id = ?1", + params![cmd_id], + )?; + Ok(()) + } + + pub fn get_command_usage(&self, cmd_id: i32) -> Result> { + let mut stmt = self + .conn + .prepare("SELECT usage_count, last_used_at FROM commands WHERE cmd_id = ?1")?; + let usage_iter = stmt.query_map(params![cmd_id], |row| Ok((row.get(0)?, row.get(1)?)))?; + let mut usages = Vec::new(); + for usage in usage_iter { + usages.push(usage?); + } + Ok(usages.first().cloned()) + } } #[cfg(test)] @@ -983,4 +1090,62 @@ mod tests { fs::remove_file(&db_path).unwrap(); } + + #[test] + fn test_command_aliases_crud() { + let dir = tempdir().unwrap(); + let db_path = dir.path().join("test.db"); + let db = JarvisDB::new(&db_path, None).unwrap(); + db.init().unwrap(); + + // Create an extension and command first + db.create_extension("test", "0.1.0", true, None, None) + .unwrap(); + let ext = db + .get_unique_extension_by_identifier("test") + .unwrap() + .unwrap(); + db.create_command(ext.ext_id, "test", CmdType::Iframe, "{}", true, None, None) + .unwrap(); + let cmd = db.get_command_by_id(1).unwrap().unwrap(); + + // Test creating command alias + db.create_command_alias(cmd.cmd_id, "test_alias").unwrap(); + db.create_command_alias(cmd.cmd_id, "another_alias") + .unwrap(); + + // Test getting command aliases by cmd_id + let aliases = db.get_command_aliases_by_cmd_id(cmd.cmd_id).unwrap(); + println!("aliases: {:#?}", aliases); + assert_eq!(aliases.len(), 2); + assert_eq!(aliases[0].alias, "another_alias"); + assert_eq!(aliases[1].alias, "test_alias"); + + // Test getting command alias by id + let alias = db + .get_command_alias_by_id(aliases[0].alias_id) + .unwrap() + .unwrap(); + assert_eq!(alias.alias, "another_alias"); + + // Test searching command aliases + let search_results = db.search_command_aliases("test").unwrap(); + println!("search_results: {:#?}", search_results); + assert_eq!(search_results.len(), 1); + assert_eq!(search_results[0].alias, "test_alias"); + + // Test deleting command alias + db.delete_command_alias_by_id(aliases[0].alias_id).unwrap(); + let aliases = db.get_command_aliases_by_cmd_id(cmd.cmd_id).unwrap(); + assert_eq!(aliases.len(), 1); + assert_eq!(aliases[0].alias, "test_alias"); + + // Test command usage tracking + db.update_command_usage(cmd.cmd_id).unwrap(); + let usage = db.get_command_usage(cmd.cmd_id).unwrap().unwrap(); + assert_eq!(usage.0, 1); // usage_count should be 1 + assert!(!usage.1.is_empty()); // last_used_at should be set + + fs::remove_file(&db_path).unwrap(); + } } diff --git a/packages/db/src/models.rs b/packages/db/src/models.rs index c767391..fbb58a4 100644 --- a/packages/db/src/models.rs +++ b/packages/db/src/models.rs @@ -70,6 +70,8 @@ pub struct Cmd { pub alias: Option, pub hotkey: Option, pub enabled: bool, + pub usage_count: i32, + pub last_used_at: Option, } #[derive(Debug, Serialize, Deserialize, Display)] @@ -122,6 +124,15 @@ pub struct ExtDataSearchQuery { pub fields: Option>, } +#[derive(Debug, Clone)] +pub struct CmdAlias { + pub alias_id: i32, + pub cmd_id: i32, + pub alias: String, + pub created_at: String, + pub updated_at: String, +} + #[cfg(test)] mod tests { use super::*; diff --git a/packages/db/src/schema.rs b/packages/db/src/schema.rs index 229671b..09c19a0 100644 --- a/packages/db/src/schema.rs +++ b/packages/db/src/schema.rs @@ -1,4 +1,5 @@ pub const SCHEMA1: &str = include_str!("../sql/2024-10-23.sql"); +pub const SCHEMA2: &str = include_str!("../sql/2025-03-04.sql"); pub struct Migration { pub version: u16, pub script: String, @@ -16,5 +17,9 @@ impl Migration { use std::sync::LazyLock; -pub static MIGRATIONS: LazyLock> = - LazyLock::new(|| vec![Migration::new(1, SCHEMA1, "Initial Migration")]); +pub static MIGRATIONS: LazyLock> = LazyLock::new(|| { + vec![ + Migration::new(1, SCHEMA1, "Initial Migration"), + Migration::new(2, SCHEMA2, "Add command aliases and usage tracking"), + ] +});