feat: optimize SQL balance calculation (#1152)

* feat: optimize SQL balance calculation

replace proof-fetching approach with SUM aggregation

- add get_balance() method to Database trait
- implement SQL SUM aggregation in cdk-sql-common
- update total_balance() to use get_balance() instead of get_unspent_proofs()
- redb impl maintains existing behavior

---------

Co-authored-by: thesimplekid <tsk@thesimplekid.com>
Co-authored-by: Cesar Rodas <cesar@rodasm.com.py>
This commit is contained in:
vnprc
2025-10-06 04:29:57 -04:00
committed by GitHub
parent a8c35dbef0
commit 1a493d61f8
8 changed files with 150 additions and 2 deletions

View File

@@ -836,6 +836,70 @@ ON CONFLICT(id) DO UPDATE SET
.collect::<Vec<_>>())
}
async fn get_balance(
&self,
mint_url: Option<MintUrl>,
unit: Option<CurrencyUnit>,
states: Option<Vec<State>>,
) -> Result<u64, Self::Err> {
let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?;
let mut query_str = "SELECT COALESCE(SUM(amount), 0) as total FROM proof".to_string();
let mut where_clauses = Vec::new();
let states = states
.unwrap_or_default()
.into_iter()
.map(|x| x.to_string())
.collect::<Vec<_>>();
if mint_url.is_some() {
where_clauses.push("mint_url = :mint_url");
}
if unit.is_some() {
where_clauses.push("unit = :unit");
}
if !states.is_empty() {
where_clauses.push("state IN (:states)");
}
if !where_clauses.is_empty() {
query_str.push_str(" WHERE ");
query_str.push_str(&where_clauses.join(" AND "));
}
let mut q = query(&query_str)?;
if let Some(ref mint_url) = mint_url {
q = q.bind("mint_url", mint_url.to_string());
}
if let Some(ref unit) = unit {
q = q.bind("unit", unit.to_string());
}
if !states.is_empty() {
q = q.bind_vec("states", states);
}
let balance = q
.pluck(&*conn)
.await?
.map(|n| {
// SQLite SUM returns INTEGER which we need to convert to u64
match n {
crate::stmt::Column::Integer(i) => Ok(i as u64),
crate::stmt::Column::Real(f) => Ok(f as u64),
_ => Err(Error::Database(Box::new(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"Invalid balance type",
)))),
}
})
.transpose()?
.unwrap_or(0);
Ok(balance)
}
async fn update_proofs_state(&self, ys: Vec<PublicKey>, state: State) -> Result<(), Self::Err> {
let conn = self.pool.get().map_err(|e| Error::Database(Box::new(e)))?;
query("UPDATE proof SET state = :state WHERE y IN (:ys)")?