intelli_shell/storage/
release.rs1use 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 #[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 #[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 #[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 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 #[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 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 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 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 let latest = storage.get_latest_stored_version().await.unwrap();
164 assert_eq!(latest.unwrap().0, Version::parse("3.0.0").unwrap());
165
166 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 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}