Skip to main content

intelli_shell/storage/
release.rs

1use chrono::{DateTime, Utc};
2use itertools::Itertools;
3use rusqlite::{OptionalExtension, params};
4use semver::Version;
5use tracing::instrument;
6
7use super::SqliteStorage;
8use crate::{errors::Result, model::IntelliShellRelease};
9
10impl SqliteStorage {
11    /// Upserts a list of releases into the database
12    #[instrument(skip(self, releases))]
13    pub async fn upsert_releases(&self, releases: Vec<IntelliShellRelease>) -> Result<()> {
14        if releases.is_empty() {
15            return Ok(());
16        }
17
18        self.client
19            .conn_mut(move |conn| {
20                let tx = conn.transaction()?;
21                {
22                    let mut stmt = tx.prepare(
23                        "INSERT OR REPLACE INTO release_info (tag, version, title, body, published_at, fetched_at)
24                        VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
25                    )?;
26
27                    for release in releases {
28                        tracing::trace!("Upserting release: {}", release.tag);
29                        stmt.execute(params![
30                            release.tag,
31                            release.version.to_string(),
32                            release.title,
33                            release.body,
34                            release.published_at,
35                            release.fetched_at,
36                        ])?;
37                    }
38                }
39                tx.commit()?;
40                Ok(())
41            })
42            .await?;
43        Ok(())
44    }
45
46    /// Retrieves releases from the database, sorted by descending version
47    #[instrument(skip_all)]
48    pub async fn get_releases(&self) -> Result<Vec<IntelliShellRelease>> {
49        self.client
50            .conn(move |conn| {
51                let query = "SELECT tag, version, title, body, published_at, fetched_at FROM release_info ORDER BY \
52                             published_at DESC";
53                tracing::trace!("Querying release versions:\n{query}");
54                Ok(conn
55                    .prepare(query)?
56                    .query_map([], |row| {
57                        Ok(IntelliShellRelease {
58                            tag: row.get(0)?,
59                            version: Version::parse(&row.get::<_, String>(1)?).expect("valid version"),
60                            title: row.get(2)?,
61                            body: row.get(3)?,
62                            published_at: row.get(4)?,
63                            fetched_at: row.get(5)?,
64                        })
65                    })?
66                    .collect::<Result<Vec<_>, _>>()?)
67            })
68            .await
69            .map(|all_releases| {
70                all_releases
71                    .into_iter()
72                    .sorted_by(|a, b| b.version.cmp(&a.version))
73                    .collect()
74            })
75    }
76
77    /// Gets the latest stored version and its fetch time
78    #[instrument(skip_all)]
79    pub async fn get_latest_stored_version(&self) -> Result<Option<(Version, DateTime<Utc>)>> {
80        self.client
81            .conn(move |conn| {
82                // Determine latest by published_at
83                let query = "SELECT version, fetched_at FROM release_info ORDER BY published_at DESC LIMIT 1";
84                tracing::trace!("Checking latest release version:\n{query}");
85                Ok(conn
86                    .query_row(query, [], |row| {
87                        Ok((
88                            Version::parse(&row.get::<_, String>(0)?).expect("valid version"),
89                            row.get(1)?,
90                        ))
91                    })
92                    .optional()?)
93            })
94            .await
95    }
96
97    /// Prunes the release history to keep only the specified number of recent releases
98    #[instrument(skip(self))]
99    pub async fn prune_releases(&self, keep: usize) -> Result<()> {
100        self.client
101            .conn_mut(move |conn| {
102                let query = "DELETE FROM release_info WHERE tag NOT IN (
103                    SELECT tag FROM release_info ORDER BY published_at DESC LIMIT ?1
104                )";
105                tracing::trace!("Pruning releases to keep {keep}:\n{query}");
106                conn.execute(query, params![keep])?;
107                Ok(())
108            })
109            .await?;
110        Ok(())
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use chrono::Utc;
117    use pretty_assertions::assert_eq;
118
119    use super::*;
120
121    #[tokio::test]
122    async fn test_release_storage_ops() {
123        let storage = SqliteStorage::new_in_memory().await.unwrap();
124
125        // 1. Initial State: Empty
126        let releases = storage.get_releases().await.unwrap();
127        assert!(releases.is_empty());
128        assert!(storage.get_latest_stored_version().await.unwrap().is_none());
129
130        // 2. Upsert Releases
131        let release_v1 = IntelliShellRelease {
132            tag: "v1.0.0".to_string(),
133            version: Version::parse("1.0.0").unwrap(),
134            title: "Release 1".to_string(),
135            body: Some("Body 1".to_string()),
136            published_at: Utc::now() - chrono::Duration::days(10),
137            fetched_at: Utc::now(),
138        };
139        let release_v2 = IntelliShellRelease {
140            tag: "v2.0.0".to_string(),
141            version: Version::parse("2.0.0").unwrap(),
142            title: "Release 2".to_string(),
143            body: None,
144            published_at: Utc::now() - chrono::Duration::days(5),
145            fetched_at: Utc::now(),
146        };
147        // v3 is newest
148        let release_v3 = IntelliShellRelease {
149            tag: "v3.0.0".to_string(),
150            version: Version::parse("3.0.0").unwrap(),
151            title: "Release 3".to_string(),
152            body: Some("Body 3".to_string()),
153            published_at: Utc::now(),
154            fetched_at: Utc::now(),
155        };
156
157        storage
158            .upsert_releases(vec![release_v1.clone(), release_v2.clone(), release_v3.clone()])
159            .await
160            .unwrap();
161
162        // 3. Get Latest
163        let latest = storage.get_latest_stored_version().await.unwrap();
164        assert_eq!(latest.unwrap().0, Version::parse("3.0.0").unwrap());
165
166        // 4. Get Releases (All, should be sorted DESC)
167        let all = storage.get_releases().await.unwrap();
168        assert_eq!(all.len(), 3);
169        assert_eq!(all[0].version, release_v3.version);
170        assert_eq!(all[1].version, release_v2.version);
171        assert_eq!(all[2].version, release_v1.version);
172
173        // 5. Prune (Keep 2) -> Should keep v3 and v2
174        storage.prune_releases(2).await.unwrap();
175        let remaining = storage.get_releases().await.unwrap();
176        assert_eq!(remaining.len(), 2);
177        assert_eq!(remaining[0].version, release_v3.version);
178        assert_eq!(remaining[1].version, release_v2.version);
179    }
180}