1use super::*;
2use crate::cluster::{Command, Reply};
3use sqlx::SqliteConnection;
4
5const LOCAL_PREFIXES: [&str; 2] = ["metrics:", "raft:"];
7
8pub(crate) fn is_local(key: &str) -> bool {
9 LOCAL_PREFIXES.iter().any(|p| key.starts_with(p))
10}
11
12pub(crate) type Row = (String, String, String);
14
15pub(crate) async fn set(
16 conn: &mut SqliteConnection,
17 now: &str,
18 key: &str,
19 value: &str,
20) -> anyhow::Result<()> {
21 sqlx::query(
22 "INSERT INTO kv (key, value, updated_at) VALUES (?, ?, ?)
23 ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at",
24 )
25 .bind(key)
26 .bind(value)
27 .bind(now)
28 .execute(conn)
29 .await?;
30 Ok(())
31}
32
33pub(crate) async fn increment(
35 conn: &mut SqliteConnection,
36 now: &str,
37 key: &str,
38) -> anyhow::Result<u64> {
39 let row: (i64,) = sqlx::query_as(
40 "INSERT INTO kv (key, value, updated_at) VALUES (?1, '1', ?2)
41 ON CONFLICT(key) DO UPDATE
42 SET value = CAST(value AS INTEGER) + 1,
43 updated_at = ?2
44 RETURNING CAST(value AS INTEGER)",
45 )
46 .bind(key)
47 .bind(now)
48 .fetch_one(conn)
49 .await?;
50 debug_assert!(
51 row.0 >= 1,
52 "kv_increment returned unexpected value {}",
53 row.0
54 );
55 Ok(row.0 as u64)
56}
57
58pub(crate) async fn insert_if_absent(
60 conn: &mut SqliteConnection,
61 now: &str,
62 key: &str,
63 value: &str,
64) -> anyhow::Result<String> {
65 sqlx::query("INSERT OR IGNORE INTO kv (key, value, updated_at) VALUES (?, ?, ?)")
66 .bind(key)
67 .bind(value)
68 .bind(now)
69 .execute(&mut *conn)
70 .await?;
71 let (stored,): (String,) = sqlx::query_as("SELECT value FROM kv WHERE key = ?")
72 .bind(key)
73 .fetch_one(conn)
74 .await?;
75 Ok(stored)
76}
77
78pub(crate) async fn delete(conn: &mut SqliteConnection, key: &str) -> anyhow::Result<()> {
79 sqlx::query("DELETE FROM kv WHERE key = ?")
80 .bind(key)
81 .execute(conn)
82 .await?;
83 Ok(())
84}
85
86pub(crate) async fn replicated_rows(pool: &SqlitePool) -> anyhow::Result<Vec<Row>> {
87 let rows: Vec<Row> = sqlx::query_as("SELECT key, value, updated_at FROM kv ORDER BY key")
88 .fetch_all(pool)
89 .await?;
90 Ok(rows.into_iter().filter(|r| !is_local(&r.0)).collect())
91}
92
93pub(crate) async fn replace_replicated(
95 conn: &mut SqliteConnection,
96 rows: &[Row],
97) -> anyhow::Result<()> {
98 let keys: Vec<(String,)> = sqlx::query_as("SELECT key FROM kv")
99 .fetch_all(&mut *conn)
100 .await?;
101 for (key,) in keys.iter().filter(|(k,)| !is_local(k)) {
102 delete(conn, key).await?;
103 }
104
105 for (key, value, updated_at) in rows {
106 set(conn, updated_at, key, value).await?;
107 }
108
109 Ok(())
110}
111
112impl TaskStore {
113 pub async fn kv_get(&self, key: &str) -> anyhow::Result<Option<String>> {
114 let result: Option<(String,)> = sqlx::query_as("SELECT value FROM kv WHERE key = ?")
115 .bind(key)
116 .fetch_optional(&self.pool)
117 .await?;
118 Ok(result.map(|r| r.0))
119 }
120
121 pub async fn kv_set(&self, key: &str, value: &str) -> anyhow::Result<()> {
123 let cmd = Command::KvSet {
124 key: key.to_string(),
125 value: value.to_string(),
126 };
127 self.write_kv(key, cmd).await.map(|_| ())
128 }
129
130 pub async fn kv_increment(&self, key: &str) -> anyhow::Result<u64> {
133 let cmd = Command::KvIncrement {
134 key: key.to_string(),
135 };
136 match self.write_kv(key, cmd).await? {
137 Reply::Count(n) => Ok(n),
138 other => anyhow::bail!("kv_increment got {other:?}"),
139 }
140 }
141
142 pub async fn kv_insert_if_absent(&self, key: &str, value: &str) -> anyhow::Result<String> {
144 let cmd = Command::KvInsertIfAbsent {
145 key: key.to_string(),
146 value: value.to_string(),
147 };
148 match self.write_kv(key, cmd).await? {
149 Reply::Value(v) => Ok(v),
150 other => anyhow::bail!("kv_insert_if_absent got {other:?}"),
151 }
152 }
153
154 pub async fn kv_delete(&self, key: &str) -> anyhow::Result<()> {
156 let cmd = Command::KvDelete {
157 key: key.to_string(),
158 };
159 self.write_kv(key, cmd).await.map(|_| ())
160 }
161
162 async fn write_kv(&self, key: &str, cmd: Command) -> anyhow::Result<Reply> {
164 if is_local(key) {
165 return self.apply_direct(cmd).await;
166 }
167
168 self.write(cmd).await
169 }
170
171 pub async fn kv_list_prefix(&self, prefix: &str) -> anyhow::Result<Vec<(String, String)>> {
176 let escaped = prefix
179 .replace('\\', "\\\\")
180 .replace('%', "\\%")
181 .replace('_', "\\_");
182 let pattern = format!("{escaped}%");
183 let rows: Vec<(String, String)> =
184 sqlx::query_as("SELECT key, value FROM kv WHERE key LIKE ? ESCAPE '\\'")
185 .bind(&pattern)
186 .fetch_all(&self.pool)
187 .await?;
188 Ok(rows)
189 }
190}