2026-03-31 19:19:04 +08:00
use async_trait ::async_trait ;
2026-05-17 14:07:23 +08:00
use sqlx ::{ PgPool , Postgres , QueryBuilder , Row };
2026-03-31 19:19:04 +08:00
2026-04-07 02:50:19 +08:00
use super ::{
2026-03-31 19:19:04 +08:00
ProviderQuotaReadRepository , ProviderQuotaWriteRepository , StoredProviderQuotaSnapshot ,
};
2026-04-07 02:50:19 +08:00
use crate ::{ error ::SqlxResultExt , DataLayerError };
2026-05-17 14:07:23 +08:00
use aether_data_query ::{ push_in , WhereClause };
2026-03-31 19:19:04 +08:00
const FIND_BY_PROVIDER_ID_SQL : & str = r #"
SELECT
id AS provider_id,
CAST(billing_type AS TEXT) AS billing_type,
CAST(monthly_quota_usd AS DOUBLE PRECISION) AS monthly_quota_usd,
CAST(COALESCE(monthly_used_usd, 0) AS DOUBLE PRECISION) AS monthly_used_usd,
quota_reset_day,
CAST(EXTRACT(EPOCH FROM quota_last_reset_at) AS BIGINT) AS quota_last_reset_at_unix_secs,
CAST(EXTRACT(EPOCH FROM quota_expires_at) AS BIGINT) AS quota_expires_at_unix_secs,
is_active
FROM providers
WHERE id = $1
LIMIT 1
"# ;
const RESET_DUE_SQL : & str = r #"
UPDATE providers
SET
monthly_used_usd = 0,
quota_last_reset_at = TO_TIMESTAMP($1::double precision),
updated_at = NOW()
WHERE
billing_type = 'monthly_quota'
AND is_active = TRUE
AND (
quota_last_reset_at IS NULL
OR (EXTRACT(EPOCH FROM TO_TIMESTAMP($1::double precision)) - EXTRACT(EPOCH FROM quota_last_reset_at)) >= (quota_reset_day * 86400)
)
"# ;
#[derive(Debug, Clone)]
pub struct SqlxProviderQuotaRepository {
pool : PgPool ,
}
impl SqlxProviderQuotaRepository {
pub fn new ( pool : PgPool ) -> Self {
Self { pool }
}
}
#[async_trait]
impl ProviderQuotaReadRepository for SqlxProviderQuotaRepository {
async fn find_by_provider_id (
& self ,
provider_id : & str ,
) -> Result < Option < StoredProviderQuotaSnapshot > , DataLayerError > {
let row = sqlx ::query ( FIND_BY_PROVIDER_ID_SQL )
. bind ( provider_id )
. fetch_optional ( & self . pool )
2026-04-07 02:50:19 +08:00
. await
. map_postgres_err () ? ;
2026-03-31 19:19:04 +08:00
row . as_ref (). map ( map_row ). transpose ()
}
2026-04-23 14:42:51 +08:00
async fn find_by_provider_ids (
& self ,
provider_ids : & [ String ],
) -> Result < Vec < StoredProviderQuotaSnapshot > , DataLayerError > {
if provider_ids . is_empty () {
return Ok ( Vec ::new ());
}
2026-05-17 14:07:23 +08:00
let mut builder = QueryBuilder ::< Postgres > ::new (
"SELECT id AS provider_id, CAST(billing_type AS TEXT) AS billing_type, CAST(monthly_quota_usd AS DOUBLE PRECISION) AS monthly_quota_usd, CAST(COALESCE(monthly_used_usd, 0) AS DOUBLE PRECISION) AS monthly_used_usd, quota_reset_day, CAST(EXTRACT(EPOCH FROM quota_last_reset_at) AS BIGINT) AS quota_last_reset_at_unix_secs, CAST(EXTRACT(EPOCH FROM quota_expires_at) AS BIGINT) AS quota_expires_at_unix_secs, is_active FROM providers" ,
);
let mut where_clause = WhereClause ::new ();
push_in ( & mut builder , & mut where_clause , "id" , provider_ids );
builder . push ( " ORDER BY id ASC" );
builder
. build ()
2026-04-23 14:42:51 +08:00
. fetch_all ( & self . pool )
. await
. map_postgres_err () ?
. iter ()
. map ( map_row )
. collect ()
}
2026-03-31 19:19:04 +08:00
}
#[async_trait]
impl ProviderQuotaWriteRepository for SqlxProviderQuotaRepository {
async fn reset_due ( & self , now_unix_secs : u64 ) -> Result < usize , DataLayerError > {
let result = sqlx ::query ( RESET_DUE_SQL )
. bind ( i64 ::try_from ( now_unix_secs ). map_err ( | _ | {
DataLayerError ::InvalidInput ( "provider quota reset timestamp overflow" . to_string ())
}) ? )
. execute ( & self . pool )
2026-04-07 02:50:19 +08:00
. await
. map_postgres_err () ? ;
2026-03-31 19:19:04 +08:00
Ok ( result . rows_affected () as usize )
}
}
fn map_row ( row : & sqlx ::postgres ::PgRow ) -> Result < StoredProviderQuotaSnapshot , DataLayerError > {
StoredProviderQuotaSnapshot ::new (
2026-04-07 02:50:19 +08:00
row . try_get ( "provider_id" ). map_postgres_err () ? ,
row . try_get ( "billing_type" ). map_postgres_err () ? ,
row . try_get ( "monthly_quota_usd" ). map_postgres_err () ? ,
row . try_get ( "monthly_used_usd" ). map_postgres_err () ? ,
row . try_get ( "quota_reset_day" ). map_postgres_err () ? ,
row . try_get ( "quota_last_reset_at_unix_secs" )
. map_postgres_err () ? ,
row . try_get ( "quota_expires_at_unix_secs" )
. map_postgres_err () ? ,
row . try_get ( "is_active" ). map_postgres_err () ? ,
2026-03-31 19:19:04 +08:00
)
}
#[cfg(test)]
mod tests {
use super ::SqlxProviderQuotaRepository ;
2026-05-05 18:27:36 +08:00
use crate ::driver ::postgres ::{ PostgresPoolConfig , PostgresPoolFactory };
2026-03-31 19:19:04 +08:00
#[tokio::test]
async fn repository_constructs_from_lazy_pool () {
let factory = PostgresPoolFactory ::new ( PostgresPoolConfig {
database_url : "postgres://localhost/aether" . to_string (),
min_connections : 1 ,
max_connections : 4 ,
acquire_timeout_ms : 1_000 ,
idle_timeout_ms : 5_000 ,
max_lifetime_ms : 30_000 ,
statement_cache_capacity : 64 ,
require_ssl : false ,
})
. expect ( "factory should build" );
let pool = factory . connect_lazy (). expect ( "pool should build" );
let _repository = SqlxProviderQuotaRepository ::new ( pool );
}
}