persistence.rs

  1use anyhow::Result;
  2use db::{
  3    query,
  4    sqlez::{
  5        bindable::Column, domain::Domain, statement::Statement,
  6        thread_safe_connection::ThreadSafeConnection,
  7    },
  8    sqlez_macros::sql,
  9};
 10use serde::{Deserialize, Serialize};
 11use time::OffsetDateTime;
 12
 13#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
 14pub(crate) struct SerializedCommandInvocation {
 15    pub(crate) command_name: String,
 16    pub(crate) user_query: String,
 17    pub(crate) last_invoked: OffsetDateTime,
 18}
 19
 20#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
 21pub(crate) struct SerializedCommandUsage {
 22    pub(crate) command_name: String,
 23    pub(crate) invocations: u16,
 24    pub(crate) last_invoked: OffsetDateTime,
 25}
 26
 27impl Column for SerializedCommandUsage {
 28    fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
 29        let (command_name, next_index): (String, i32) = Column::column(statement, start_index)?;
 30        let (invocations, next_index): (u16, i32) = Column::column(statement, next_index)?;
 31        let (last_invoked_raw, next_index): (i64, i32) = Column::column(statement, next_index)?;
 32
 33        let usage = Self {
 34            command_name,
 35            invocations,
 36            last_invoked: OffsetDateTime::from_unix_timestamp(last_invoked_raw)?,
 37        };
 38        Ok((usage, next_index))
 39    }
 40}
 41
 42impl Column for SerializedCommandInvocation {
 43    fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
 44        let (command_name, next_index): (String, i32) = Column::column(statement, start_index)?;
 45        let (user_query, next_index): (String, i32) = Column::column(statement, next_index)?;
 46        let (last_invoked_raw, next_index): (i64, i32) = Column::column(statement, next_index)?;
 47        let command_invocation = Self {
 48            command_name,
 49            user_query,
 50            last_invoked: OffsetDateTime::from_unix_timestamp(last_invoked_raw)?,
 51        };
 52        Ok((command_invocation, next_index))
 53    }
 54}
 55
 56pub struct CommandPaletteDB(ThreadSafeConnection);
 57
 58impl Domain for CommandPaletteDB {
 59    const NAME: &str = stringify!(CommandPaletteDB);
 60    const MIGRATIONS: &[&str] = &[sql!(
 61        CREATE TABLE IF NOT EXISTS command_invocations(
 62            id INTEGER PRIMARY KEY AUTOINCREMENT,
 63            command_name TEXT NOT NULL,
 64            user_query TEXT NOT NULL,
 65            last_invoked INTEGER DEFAULT (unixepoch())  NOT NULL
 66        ) STRICT;
 67    )];
 68}
 69
 70db::static_connection!(COMMAND_PALETTE_HISTORY, CommandPaletteDB, []);
 71
 72impl CommandPaletteDB {
 73    pub async fn write_command_invocation(
 74        &self,
 75        command_name: impl Into<String>,
 76        user_query: impl Into<String>,
 77    ) -> Result<()> {
 78        let command_name = command_name.into();
 79        let user_query = user_query.into();
 80        log::debug!(
 81            "Writing command invocation: command_name={command_name}, user_query={user_query}"
 82        );
 83        self.write_command_invocation_internal(command_name, user_query)
 84            .await
 85    }
 86
 87    query! {
 88        pub fn get_last_invoked(command: &str) -> Result<Option<SerializedCommandInvocation>> {
 89            SELECT
 90            command_name,
 91            user_query,
 92            last_invoked FROM command_invocations
 93            WHERE command_name=(?)
 94            ORDER BY last_invoked DESC
 95            LIMIT 1
 96        }
 97    }
 98
 99    query! {
100        pub fn get_command_usage(command: &str) -> Result<Option<SerializedCommandUsage>> {
101            SELECT command_name, COUNT(1), MAX(last_invoked)
102            FROM command_invocations
103            WHERE command_name=(?)
104            GROUP BY command_name
105        }
106    }
107
108    query! {
109        async fn write_command_invocation_internal(command_name: String, user_query: String) -> Result<()> {
110            INSERT INTO command_invocations (command_name, user_query) VALUES ((?), (?));
111            DELETE FROM command_invocations WHERE id IN (SELECT MIN(id) FROM command_invocations HAVING COUNT(1) > 1000);
112        }
113    }
114
115    query! {
116        pub fn list_commands_used() -> Result<Vec<SerializedCommandUsage>> {
117            SELECT command_name, COUNT(1), MAX(last_invoked)
118            FROM command_invocations
119            GROUP BY command_name
120            ORDER BY COUNT(1) DESC
121        }
122    }
123}
124
125#[cfg(test)]
126mod tests {
127
128    use crate::persistence::{CommandPaletteDB, SerializedCommandUsage};
129
130    #[gpui::test]
131    async fn test_saves_and_retrieves_command_invocation() {
132        let db =
133            CommandPaletteDB::open_test_db("test_saves_and_retrieves_command_invocation").await;
134
135        let retrieved_cmd = db.get_last_invoked("editor: backspace").unwrap();
136
137        assert!(retrieved_cmd.is_none());
138
139        db.write_command_invocation("editor: backspace", "")
140            .await
141            .unwrap();
142
143        let retrieved_cmd = db.get_last_invoked("editor: backspace").unwrap();
144
145        assert!(retrieved_cmd.is_some());
146        let retrieved_cmd = retrieved_cmd.expect("is some");
147        assert_eq!(retrieved_cmd.command_name, "editor: backspace".to_string());
148        assert_eq!(retrieved_cmd.user_query, "".to_string());
149    }
150
151    #[gpui::test]
152    async fn test_gets_usage_history() {
153        let db = CommandPaletteDB::open_test_db("test_gets_usage_history").await;
154        db.write_command_invocation("go to line: toggle", "200")
155            .await
156            .unwrap();
157        db.write_command_invocation("go to line: toggle", "201")
158            .await
159            .unwrap();
160
161        let retrieved_cmd = db.get_last_invoked("go to line: toggle").unwrap();
162
163        assert!(retrieved_cmd.is_some());
164        let retrieved_cmd = retrieved_cmd.expect("is some");
165
166        let command_usage = db.get_command_usage("go to line: toggle").unwrap();
167
168        assert!(command_usage.is_some());
169        let command_usage: SerializedCommandUsage = command_usage.expect("is some");
170
171        assert_eq!(command_usage.command_name, "go to line: toggle");
172        assert_eq!(command_usage.invocations, 2);
173        assert_eq!(command_usage.last_invoked, retrieved_cmd.last_invoked);
174    }
175
176    #[gpui::test]
177    async fn test_lists_ordered_by_usage() {
178        let db = CommandPaletteDB::open_test_db("test_lists_ordered_by_usage").await;
179
180        let empty_commands = db.list_commands_used();
181        match &empty_commands {
182            Ok(_) => (),
183            Err(e) => println!("Error: {:?}", e),
184        }
185        assert!(empty_commands.is_ok());
186        assert_eq!(empty_commands.expect("is ok").len(), 0);
187
188        db.write_command_invocation("go to line: toggle", "200")
189            .await
190            .unwrap();
191        db.write_command_invocation("editor: backspace", "")
192            .await
193            .unwrap();
194        db.write_command_invocation("editor: backspace", "")
195            .await
196            .unwrap();
197
198        let commands = db.list_commands_used();
199
200        assert!(commands.is_ok());
201        let commands = commands.expect("is ok");
202        assert_eq!(commands.len(), 2);
203        assert_eq!(commands.as_slice()[0].command_name, "editor: backspace");
204        assert_eq!(commands.as_slice()[0].invocations, 2);
205        assert_eq!(commands.as_slice()[1].command_name, "go to line: toggle");
206        assert_eq!(commands.as_slice()[1].invocations, 1);
207    }
208
209    #[gpui::test]
210    async fn test_handles_max_invocation_entries() {
211        let db = CommandPaletteDB::open_test_db("test_handles_max_invocation_entries").await;
212
213        for i in 1..=1001 {
214            db.write_command_invocation("some-command", &i.to_string())
215                .await
216                .unwrap();
217        }
218        let some_command = db.get_command_usage("some-command").unwrap();
219
220        assert!(some_command.is_some());
221        assert_eq!(some_command.expect("is some").invocations, 1000);
222    }
223}