use super::*;
use crate::cluster::{Command, Reply};
use sqlx::SqliteConnection;
const LOCAL_PREFIXES: [&str; 2] = ["metrics:", "raft:"];
pub(crate) fn is_local(key: &str) -> bool {
LOCAL_PREFIXES.iter().any(|p| key.starts_with(p))
}
pub(crate) type Row = (String, String, String);
pub(crate) async fn set(
conn: &mut SqliteConnection,
now: &str,
key: &str,
value: &str,
) -> anyhow::Result<()> {
sqlx::query(
"INSERT INTO kv (key, value, updated_at) VALUES (?, ?, ?)
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at",
)
.bind(key)
.bind(value)
.bind(now)
.execute(conn)
.await?;
Ok(())
}
pub(crate) async fn increment(
conn: &mut SqliteConnection,
now: &str,
key: &str,
) -> anyhow::Result<u64> {
let row: (i64,) = sqlx::query_as(
"INSERT INTO kv (key, value, updated_at) VALUES (?1, '1', ?2)
ON CONFLICT(key) DO UPDATE
SET value = CAST(value AS INTEGER) + 1,
updated_at = ?2
RETURNING CAST(value AS INTEGER)",
)
.bind(key)
.bind(now)
.fetch_one(conn)
.await?;
debug_assert!(
row.0 >= 1,
"kv_increment returned unexpected value {}",
row.0
);
Ok(row.0 as u64)
}
pub(crate) async fn insert_if_absent(
conn: &mut SqliteConnection,
now: &str,
key: &str,
value: &str,
) -> anyhow::Result<String> {
sqlx::query("INSERT OR IGNORE INTO kv (key, value, updated_at) VALUES (?, ?, ?)")
.bind(key)
.bind(value)
.bind(now)
.execute(&mut *conn)
.await?;
let (stored,): (String,) = sqlx::query_as("SELECT value FROM kv WHERE key = ?")
.bind(key)
.fetch_one(conn)
.await?;
Ok(stored)
}
pub(crate) async fn delete(conn: &mut SqliteConnection, key: &str) -> anyhow::Result<()> {
sqlx::query("DELETE FROM kv WHERE key = ?")
.bind(key)
.execute(conn)
.await?;
Ok(())
}
pub(crate) async fn replicated_rows(pool: &SqlitePool) -> anyhow::Result<Vec<Row>> {
let rows: Vec<Row> = sqlx::query_as("SELECT key, value, updated_at FROM kv ORDER BY key")
.fetch_all(pool)
.await?;
Ok(rows.into_iter().filter(|r| !is_local(&r.0)).collect())
}
pub(crate) async fn replace_replicated(
conn: &mut SqliteConnection,
rows: &[Row],
) -> anyhow::Result<()> {
let keys: Vec<(String,)> = sqlx::query_as("SELECT key FROM kv")
.fetch_all(&mut *conn)
.await?;
for (key,) in keys.iter().filter(|(k,)| !is_local(k)) {
delete(conn, key).await?;
}
for (key, value, updated_at) in rows {
set(conn, updated_at, key, value).await?;
}
Ok(())
}
impl TaskStore {
pub async fn kv_get(&self, key: &str) -> anyhow::Result<Option<String>> {
let result: Option<(String,)> = sqlx::query_as("SELECT value FROM kv WHERE key = ?")
.bind(key)
.fetch_optional(&self.pool)
.await?;
Ok(result.map(|r| r.0))
}
pub async fn kv_set(&self, key: &str, value: &str) -> anyhow::Result<()> {
let cmd = Command::KvSet {
key: key.to_string(),
value: value.to_string(),
};
self.write_kv(key, cmd).await.map(|_| ())
}
pub async fn kv_increment(&self, key: &str) -> anyhow::Result<u64> {
let cmd = Command::KvIncrement {
key: key.to_string(),
};
match self.write_kv(key, cmd).await? {
Reply::Count(n) => Ok(n),
other => anyhow::bail!("kv_increment got {other:?}"),
}
}
pub async fn kv_insert_if_absent(&self, key: &str, value: &str) -> anyhow::Result<String> {
let cmd = Command::KvInsertIfAbsent {
key: key.to_string(),
value: value.to_string(),
};
match self.write_kv(key, cmd).await? {
Reply::Value(v) => Ok(v),
other => anyhow::bail!("kv_insert_if_absent got {other:?}"),
}
}
pub async fn kv_delete(&self, key: &str) -> anyhow::Result<()> {
let cmd = Command::KvDelete {
key: key.to_string(),
};
self.write_kv(key, cmd).await.map(|_| ())
}
async fn write_kv(&self, key: &str, cmd: Command) -> anyhow::Result<Reply> {
if is_local(key) {
return self.apply_direct(cmd).await;
}
self.write(cmd).await
}
pub async fn kv_list_prefix(&self, prefix: &str) -> anyhow::Result<Vec<(String, String)>> {
let escaped = prefix
.replace('\\', "\\\\")
.replace('%', "\\%")
.replace('_', "\\_");
let pattern = format!("{escaped}%");
let rows: Vec<(String, String)> =
sqlx::query_as("SELECT key, value FROM kv WHERE key LIKE ? ESCAPE '\\'")
.bind(&pattern)
.fetch_all(&self.pool)
.await?;
Ok(rows)
}
}