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
This commit is contained in:
Huakun Shen 2025-03-06 15:51:21 -05:00
parent b18d8b9e32
commit b13d1350d6
No known key found for this signature in database
4 changed files with 231 additions and 6 deletions

View File

@ -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;

View File

@ -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<P: AsRef<Path>>(
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<Option<models::Cmd>> {
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<Vec<models::Cmd>> {
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<Vec<models::CmdAlias>> {
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<Option<models::CmdAlias>> {
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<Vec<models::CmdAlias>> {
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<Option<(i32, String)>> {
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();
}
}

View File

@ -70,6 +70,8 @@ pub struct Cmd {
pub alias: Option<String>,
pub hotkey: Option<String>,
pub enabled: bool,
pub usage_count: i32,
pub last_used_at: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Display)]
@ -122,6 +124,15 @@ pub struct ExtDataSearchQuery {
pub fields: Option<Vec<ExtDataField>>,
}
#[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::*;

View File

@ -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<Vec<Migration>> =
LazyLock::new(|| vec![Migration::new(1, SCHEMA1, "Initial Migration")]);
pub static MIGRATIONS: LazyLock<Vec<Migration>> = LazyLock::new(|| {
vec![
Migration::new(1, SCHEMA1, "Initial Migration"),
Migration::new(2, SCHEMA2, "Add command aliases and usage tracking"),
]
});