use sqlx::{SqliteConnection, SqlitePool};
pub(crate) const TABLES: [&str; 3] = ["task_dependencies", "job_state", "channel_subscriptions"];
pub(crate) fn json_row_sql(cols: &[&String]) -> String {
cols.chunks(40)
.map(|chunk| {
let pairs: Vec<String> = chunk.iter().map(|c| format!("'{c}', {c}")).collect();
format!("json_object({})", pairs.join(", "))
})
.reduce(|a, b| format!("json_patch({a}, {b})"))
.unwrap_or_else(|| "json_object()".to_string())
}
async fn columns(conn: &mut SqliteConnection, table: &str) -> anyhow::Result<Vec<String>> {
anyhow::ensure!(TABLES.contains(&table), "{table} is not a replicated table");
let cols: Vec<(String,)> = sqlx::query_as("SELECT name FROM pragma_table_info(?)")
.bind(table)
.fetch_all(conn)
.await?;
Ok(cols.into_iter().map(|(c,)| c).collect())
}
pub(crate) async fn table_rows(
pool: &SqlitePool,
table: &str,
) -> anyhow::Result<Vec<serde_json::Value>> {
let mut conn = pool.acquire().await?;
let cols = columns(&mut conn, table).await?;
let refs: Vec<&String> = cols.iter().collect();
let sql = format!("SELECT {} FROM {table} ORDER BY rowid", json_row_sql(&refs));
let rows: Vec<(String,)> = sqlx::query_as(&sql).fetch_all(&mut *conn).await?;
rows.into_iter()
.map(|(r,)| Ok(serde_json::from_str(&r)?))
.collect()
}
pub(crate) async fn replace_table(
conn: &mut SqliteConnection,
table: &str,
rows: &[serde_json::Value],
) -> anyhow::Result<()> {
let allowed = columns(conn, table).await?;
sqlx::query(&format!("DELETE FROM {table}"))
.execute(&mut *conn)
.await?;
for row in rows {
let obj = row
.as_object()
.ok_or_else(|| anyhow::anyhow!("snapshot {table} row is not an object"))?;
let cols: Vec<&str> = obj.keys().map(String::as_str).collect();
for c in &cols {
anyhow::ensure!(
allowed.iter().any(|a| a == c),
"snapshot names unknown {table} column {c}"
);
}
let values: Vec<String> = cols
.iter()
.map(|c| format!("json_extract(?1, '$.\"{c}\"')"))
.collect();
let sql = format!(
"INSERT INTO {table} ({}) VALUES ({})",
cols.join(", "),
values.join(", ")
);
sqlx::query(&sql)
.bind(serde_json::to_string(row)?)
.execute(&mut *conn)
.await?;
}
Ok(())
}