use async_trait::async_trait; use sqlx::{PgPool, Postgres, Row}; use aether_data_contracts::repository::quota::{ ProviderQuotaReadRepository, ProviderQuotaWriteRepository, StoredProviderQuotaSnapshot, }; use aether_data_query::{DialectSql, SelectColumn, SelectQuery, SqlDialect}; use crate::{error::SqlxResultExt, DataLayerError}; fn quota_snapshot_select() -> SelectQuery<'static> { SelectQuery::new("providers").select_columns([ SelectColumn::expr("id").alias("provider_id"), SelectColumn::expr( DialectSql::common("billing_type").with_postgres("CAST(billing_type AS TEXT)"), ) .alias("billing_type"), SelectColumn::expr(DialectSql::common( "CAST(monthly_quota_usd AS DOUBLE PRECISION)", )) .alias("monthly_quota_usd"), SelectColumn::expr(DialectSql::common( "CAST(COALESCE(monthly_used_usd, 0) AS DOUBLE PRECISION)", )) .alias("monthly_used_usd"), SelectColumn::expr("quota_reset_day"), SelectColumn::expr(DialectSql::common( "CAST(EXTRACT(EPOCH FROM quota_last_reset_at) AS BIGINT)", )) .alias("quota_last_reset_at_unix_secs"), SelectColumn::expr(DialectSql::common( "CAST(EXTRACT(EPOCH FROM quota_expires_at) AS BIGINT)", )) .alias("quota_expires_at_unix_secs"), SelectColumn::expr("is_active"), ]) } 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, DataLayerError> { let mut statement = quota_snapshot_select().statement::(SqlDialect::Postgres); statement.where_eq("id", provider_id.to_string()).limit(1); let row = statement .finish() .build() .fetch_optional(&self.pool) .await .map_postgres_err()?; row.as_ref().map(map_row).transpose() } async fn find_by_provider_ids( &self, provider_ids: &[String], ) -> Result, DataLayerError> { if provider_ids.is_empty() { return Ok(Vec::new()); } let mut statement = quota_snapshot_select().statement::(SqlDialect::Postgres); statement .where_in("id", provider_ids) .order_by_sql("id ASC"); statement .finish() .build() .fetch_all(&self.pool) .await .map_postgres_err()? .iter() .map(map_row) .collect() } } #[async_trait] impl ProviderQuotaWriteRepository for SqlxProviderQuotaRepository { async fn reset_due(&self, now_unix_secs: u64) -> Result { 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) .await .map_postgres_err()?; Ok(result.rows_affected() as usize) } } fn map_row(row: &sqlx::postgres::PgRow) -> Result { StoredProviderQuotaSnapshot::new( 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()?, ) } #[cfg(test)] mod tests { use super::SqlxProviderQuotaRepository; use crate::{PostgresPoolConfig, PostgresPoolFactory}; #[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); } }