mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-06 17:37:47 +08:00
Use PostgreSQL as the only database backend across runtime, schema tooling, installation, Compose, and CI. Update regression tests and reject removed drivers explicitly.
158 lines
5.1 KiB
Rust
158 lines
5.1 KiB
Rust
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<Option<StoredProviderQuotaSnapshot>, DataLayerError> {
|
|
let mut statement = quota_snapshot_select().statement::<Postgres>(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<Vec<StoredProviderQuotaSnapshot>, DataLayerError> {
|
|
if provider_ids.is_empty() {
|
|
return Ok(Vec::new());
|
|
}
|
|
|
|
let mut statement = quota_snapshot_select().statement::<Postgres>(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<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)
|
|
.await
|
|
.map_postgres_err()?;
|
|
Ok(result.rows_affected() as usize)
|
|
}
|
|
}
|
|
|
|
fn map_row(row: &sqlx::postgres::PgRow) -> Result<StoredProviderQuotaSnapshot, DataLayerError> {
|
|
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);
|
|
}
|
|
}
|