mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
refactor(runtime): 优化管理端摘要查询与维护聚合链路
- 为 provider catalog key 和 video task 列表增加 summary/page 查询与排序能力,减少列表场景读取重字段 - 将多处 SQL 结果读取改为流式收集,降低 `fetch_all` 的内存占用 - 把日/小时统计、钱包日用量等维护任务改为数据库侧 `CTE + upsert` 聚合 - 修复视频任务轮询更新时从本地 snapshot 回填稀疏字段,避免 `prompt` 和请求体信息丢失
This commit is contained in:
@@ -63,6 +63,7 @@ use crate::repository::wallet::{
|
||||
SqlxWalletRepository, WalletReadRepository, WalletWriteRepository,
|
||||
};
|
||||
use crate::DataLayerError;
|
||||
use futures_util::TryStreamExt;
|
||||
use sqlx::Row;
|
||||
|
||||
const FIND_SYSTEM_CONFIG_VALUE_SQL: &str = r#"
|
||||
@@ -332,23 +333,20 @@ impl PostgresBackend {
|
||||
pub async fn list_system_config_entries(
|
||||
&self,
|
||||
) -> Result<Vec<StoredSystemConfigEntry>, DataLayerError> {
|
||||
let rows = sqlx::query(LIST_SYSTEM_CONFIG_ENTRIES_SQL)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
rows.into_iter()
|
||||
.map(|row| {
|
||||
Ok(StoredSystemConfigEntry {
|
||||
key: row.try_get("key").map_postgres_err()?,
|
||||
value: row.try_get("value").map_postgres_err()?,
|
||||
description: row.try_get("description").map_postgres_err()?,
|
||||
updated_at_unix_secs: row
|
||||
.try_get::<Option<i64>, _>("updated_at_unix_secs")
|
||||
.map_postgres_err()?
|
||||
.map(|value| value.max(0) as u64),
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, DataLayerError>>()
|
||||
let mut rows = sqlx::query(LIST_SYSTEM_CONFIG_ENTRIES_SQL).fetch(&self.pool);
|
||||
let mut entries = Vec::new();
|
||||
while let Some(row) = rows.try_next().await.map_postgres_err()? {
|
||||
entries.push(StoredSystemConfigEntry {
|
||||
key: row.try_get("key").map_postgres_err()?,
|
||||
value: row.try_get("value").map_postgres_err()?,
|
||||
description: row.try_get("description").map_postgres_err()?,
|
||||
updated_at_unix_secs: row
|
||||
.try_get::<Option<i64>, _>("updated_at_unix_secs")
|
||||
.map_postgres_err()?
|
||||
.map(|value| value.max(0) as u64),
|
||||
});
|
||||
}
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
pub async fn upsert_system_config_entry(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::error::SqlxResultExt;
|
||||
use crate::postgres::{DatabaseRecordId, PostgresTransactionOptions, PostgresTransactionRunner};
|
||||
use crate::DataLayerError;
|
||||
use futures_util::FutureExt;
|
||||
use futures_util::{FutureExt, TryStreamExt};
|
||||
use sqlx::query_scalar;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -105,13 +105,15 @@ impl PostgresLeaseRunner {
|
||||
self.transaction_runner
|
||||
.run(tx_options, |tx| {
|
||||
async move {
|
||||
let rows = query_scalar::<_, String>(&sql)
|
||||
let mut rows = query_scalar::<_, String>(&sql)
|
||||
.bind(owner)
|
||||
.bind(lease_ms)
|
||||
.fetch_all(&mut **tx)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
Ok(rows.into_iter().map(DatabaseRecordId).collect())
|
||||
.fetch(&mut **tx);
|
||||
let mut ids = Vec::new();
|
||||
while let Some(id) = rows.try_next().await.map_postgres_err()? {
|
||||
ids.push(DatabaseRecordId(id));
|
||||
}
|
||||
Ok(ids)
|
||||
}
|
||||
.boxed()
|
||||
})
|
||||
@@ -140,13 +142,15 @@ impl PostgresLeaseRunner {
|
||||
self.transaction_runner
|
||||
.run(tx_options, |tx| {
|
||||
async move {
|
||||
let rows = query_scalar::<_, String>(&sql)
|
||||
let mut rows = query_scalar::<_, String>(&sql)
|
||||
.bind(ids)
|
||||
.bind(owner)
|
||||
.fetch_all(&mut **tx)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
Ok(rows.into_iter().map(DatabaseRecordId).collect())
|
||||
.fetch(&mut **tx);
|
||||
let mut released = Vec::new();
|
||||
while let Some(id) = rows.try_next().await.map_postgres_err()? {
|
||||
released.push(DatabaseRecordId(id));
|
||||
}
|
||||
Ok(released)
|
||||
}
|
||||
.boxed()
|
||||
})
|
||||
@@ -184,14 +188,16 @@ impl PostgresLeaseRunner {
|
||||
self.transaction_runner
|
||||
.run(tx_options, |tx| {
|
||||
async move {
|
||||
let rows = query_scalar::<_, String>(&sql)
|
||||
let mut rows = query_scalar::<_, String>(&sql)
|
||||
.bind(ids)
|
||||
.bind(owner)
|
||||
.bind(lease_ms)
|
||||
.fetch_all(&mut **tx)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
Ok(rows.into_iter().map(DatabaseRecordId).collect())
|
||||
.fetch(&mut **tx);
|
||||
let mut renewed = Vec::new();
|
||||
while let Some(id) = rows.try_next().await.map_postgres_err()? {
|
||||
renewed.push(DatabaseRecordId(id));
|
||||
}
|
||||
Ok(renewed)
|
||||
}
|
||||
.boxed()
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use async_trait::async_trait;
|
||||
use chrono::{TimeZone, Utc};
|
||||
use futures_util::TryStreamExt;
|
||||
use sqlx::{postgres::PgRow, PgPool, Row};
|
||||
|
||||
use super::types::{
|
||||
@@ -220,18 +221,16 @@ impl AnnouncementReadRepository for SqlxAnnouncementReadRepository {
|
||||
.map_postgres_err()?
|
||||
.max(0) as u64;
|
||||
|
||||
let rows = sqlx::query(LIST_ANNOUNCEMENTS_SQL)
|
||||
let mut rows = sqlx::query(LIST_ANNOUNCEMENTS_SQL)
|
||||
.bind(query.active_only)
|
||||
.bind(now_unix_secs as f64)
|
||||
.bind(query.offset as i64)
|
||||
.bind(query.limit as i64)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
let items = rows
|
||||
.iter()
|
||||
.map(map_announcement_row)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
.fetch(&self.pool);
|
||||
let mut items = Vec::new();
|
||||
while let Some(row) = rows.try_next().await.map_postgres_err()? {
|
||||
items.push(map_announcement_row(&row)?);
|
||||
}
|
||||
|
||||
Ok(StoredAnnouncementPage { items, total })
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use async_trait::async_trait;
|
||||
use sqlx::{PgPool, Row};
|
||||
use futures_util::{stream::TryStream, TryStreamExt};
|
||||
use sqlx::{postgres::PgRow, PgPool, Row};
|
||||
|
||||
use super::types::{
|
||||
AuthApiKeyExportSummary, AuthApiKeyLookupKey, AuthApiKeyReadRepository,
|
||||
@@ -654,6 +655,20 @@ impl SqlxAuthApiKeySnapshotReadRepository {
|
||||
&self.pool
|
||||
}
|
||||
|
||||
async fn collect_query_rows<T, S>(
|
||||
mut rows: S,
|
||||
map_row: fn(&PgRow) -> Result<T, DataLayerError>,
|
||||
) -> Result<Vec<T>, DataLayerError>
|
||||
where
|
||||
S: TryStream<Ok = PgRow, Error = sqlx::Error> + Unpin,
|
||||
{
|
||||
let mut items = Vec::new();
|
||||
while let Some(row) = rows.try_next().await.map_postgres_err()? {
|
||||
items.push(map_row(&row)?);
|
||||
}
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
pub async fn find_api_key_snapshot(
|
||||
&self,
|
||||
key: AuthApiKeyLookupKey<'_>,
|
||||
@@ -691,12 +706,13 @@ impl SqlxAuthApiKeySnapshotReadRepository {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let rows = sqlx::query(LIST_BY_API_KEY_IDS_SQL)
|
||||
.bind(api_key_ids)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
rows.iter().map(map_auth_api_key_snapshot_row).collect()
|
||||
Self::collect_query_rows(
|
||||
sqlx::query(LIST_BY_API_KEY_IDS_SQL)
|
||||
.bind(api_key_ids)
|
||||
.fetch(&self.pool),
|
||||
map_auth_api_key_snapshot_row,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn list_export_api_keys_by_user_ids(
|
||||
@@ -707,12 +723,13 @@ impl SqlxAuthApiKeySnapshotReadRepository {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let rows = sqlx::query(LIST_EXPORT_BY_USER_IDS_SQL)
|
||||
.bind(user_ids)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
rows.iter().map(map_auth_api_key_export_row).collect()
|
||||
Self::collect_query_rows(
|
||||
sqlx::query(LIST_EXPORT_BY_USER_IDS_SQL)
|
||||
.bind(user_ids)
|
||||
.fetch(&self.pool),
|
||||
map_auth_api_key_export_row,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn list_export_api_keys_by_ids(
|
||||
@@ -723,12 +740,13 @@ impl SqlxAuthApiKeySnapshotReadRepository {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let rows = sqlx::query(LIST_EXPORT_BY_API_KEY_IDS_SQL)
|
||||
.bind(api_key_ids)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
rows.iter().map(map_auth_api_key_export_row).collect()
|
||||
Self::collect_query_rows(
|
||||
sqlx::query(LIST_EXPORT_BY_API_KEY_IDS_SQL)
|
||||
.bind(api_key_ids)
|
||||
.fetch(&self.pool),
|
||||
map_auth_api_key_export_row,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn summarize_export_api_keys_by_user_ids(
|
||||
@@ -770,11 +788,11 @@ impl SqlxAuthApiKeySnapshotReadRepository {
|
||||
pub async fn list_export_standalone_api_keys(
|
||||
&self,
|
||||
) -> Result<Vec<StoredAuthApiKeyExportRecord>, DataLayerError> {
|
||||
let rows = sqlx::query(LIST_EXPORT_STANDALONE_SQL)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
rows.iter().map(map_auth_api_key_export_row).collect()
|
||||
Self::collect_query_rows(
|
||||
sqlx::query(LIST_EXPORT_STANDALONE_SQL).fetch(&self.pool),
|
||||
map_auth_api_key_export_row,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn list_export_standalone_api_keys_page(
|
||||
@@ -785,14 +803,15 @@ impl SqlxAuthApiKeySnapshotReadRepository {
|
||||
.map_err(|_| DataLayerError::InvalidInput("limit is too large".to_string()))?;
|
||||
let skip = i64::try_from(query.skip)
|
||||
.map_err(|_| DataLayerError::InvalidInput("skip is too large".to_string()))?;
|
||||
let rows = sqlx::query(LIST_EXPORT_STANDALONE_PAGE_SQL)
|
||||
.bind(query.is_active)
|
||||
.bind(skip)
|
||||
.bind(limit)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
rows.iter().map(map_auth_api_key_export_row).collect()
|
||||
Self::collect_query_rows(
|
||||
sqlx::query(LIST_EXPORT_STANDALONE_PAGE_SQL)
|
||||
.bind(query.is_active)
|
||||
.bind(skip)
|
||||
.bind(limit)
|
||||
.fetch(&self.pool),
|
||||
map_auth_api_key_export_row,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn count_export_standalone_api_keys(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use async_trait::async_trait;
|
||||
use futures_util::{stream::TryStream, TryStreamExt};
|
||||
use sqlx::{postgres::PgRow, PgPool, Row};
|
||||
|
||||
use super::types::{
|
||||
@@ -145,16 +146,30 @@ impl SqlxAuthModuleRepository {
|
||||
}
|
||||
}
|
||||
|
||||
async fn collect_query_rows<T, S>(
|
||||
mut rows: S,
|
||||
map_row: fn(&PgRow) -> Result<T, DataLayerError>,
|
||||
) -> Result<Vec<T>, DataLayerError>
|
||||
where
|
||||
S: TryStream<Ok = PgRow, Error = sqlx::Error> + Unpin,
|
||||
{
|
||||
let mut items = Vec::new();
|
||||
while let Some(row) = rows.try_next().await.map_postgres_err()? {
|
||||
items.push(map_row(&row)?);
|
||||
}
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl AuthModuleReadRepository for SqlxAuthModuleReadRepository {
|
||||
async fn list_enabled_oauth_providers(
|
||||
&self,
|
||||
) -> Result<Vec<StoredOAuthProviderModuleConfig>, DataLayerError> {
|
||||
let rows = sqlx::query(LIST_ENABLED_OAUTH_PROVIDERS_SQL)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
rows.iter().map(map_oauth_row).collect()
|
||||
collect_query_rows(
|
||||
sqlx::query(LIST_ENABLED_OAUTH_PROVIDERS_SQL).fetch(&self.pool),
|
||||
map_oauth_row,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_ldap_config(&self) -> Result<Option<StoredLdapModuleConfig>, DataLayerError> {
|
||||
@@ -171,11 +186,11 @@ impl AuthModuleReadRepository for SqlxAuthModuleRepository {
|
||||
async fn list_enabled_oauth_providers(
|
||||
&self,
|
||||
) -> Result<Vec<StoredOAuthProviderModuleConfig>, DataLayerError> {
|
||||
let rows = sqlx::query(LIST_ENABLED_OAUTH_PROVIDERS_SQL)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
rows.iter().map(map_oauth_row).collect()
|
||||
collect_query_rows(
|
||||
sqlx::query(LIST_ENABLED_OAUTH_PROVIDERS_SQL).fetch(&self.pool),
|
||||
map_oauth_row,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_ldap_config(&self) -> Result<Option<StoredLdapModuleConfig>, DataLayerError> {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use async_trait::async_trait;
|
||||
use futures_util::{stream::TryStream, TryStreamExt};
|
||||
use sqlx::{PgPool, Row};
|
||||
|
||||
use super::{
|
||||
@@ -165,16 +166,31 @@ impl SqlxMinimalCandidateSelectionReadRepository {
|
||||
&self.pool
|
||||
}
|
||||
|
||||
async fn collect_query_rows<T, S>(
|
||||
mut rows: S,
|
||||
map_row: fn(&sqlx::postgres::PgRow) -> Result<T, DataLayerError>,
|
||||
) -> Result<Vec<T>, DataLayerError>
|
||||
where
|
||||
S: TryStream<Ok = sqlx::postgres::PgRow, Error = sqlx::Error> + Unpin,
|
||||
{
|
||||
let mut items = Vec::new();
|
||||
while let Some(row) = rows.try_next().await.map_postgres_err()? {
|
||||
items.push(map_row(&row)?);
|
||||
}
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
pub async fn list_for_exact_api_format(
|
||||
&self,
|
||||
api_format: &str,
|
||||
) -> Result<Vec<StoredMinimalCandidateSelectionRow>, DataLayerError> {
|
||||
let rows = sqlx::query(LIST_FOR_EXACT_API_FORMAT_SQL)
|
||||
.bind(api_format)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
rows.iter().map(map_candidate_selection_row).collect()
|
||||
Self::collect_query_rows(
|
||||
sqlx::query(LIST_FOR_EXACT_API_FORMAT_SQL)
|
||||
.bind(api_format)
|
||||
.fetch(&self.pool),
|
||||
map_candidate_selection_row,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn list_for_exact_api_format_and_global_model(
|
||||
@@ -182,13 +198,14 @@ impl SqlxMinimalCandidateSelectionReadRepository {
|
||||
api_format: &str,
|
||||
global_model_name: &str,
|
||||
) -> Result<Vec<StoredMinimalCandidateSelectionRow>, DataLayerError> {
|
||||
let rows = sqlx::query(LIST_FOR_EXACT_API_FORMAT_AND_GLOBAL_MODEL_SQL)
|
||||
.bind(api_format)
|
||||
.bind(global_model_name)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
rows.iter().map(map_candidate_selection_row).collect()
|
||||
Self::collect_query_rows(
|
||||
sqlx::query(LIST_FOR_EXACT_API_FORMAT_AND_GLOBAL_MODEL_SQL)
|
||||
.bind(api_format)
|
||||
.bind(global_model_name)
|
||||
.fetch(&self.pool),
|
||||
map_candidate_selection_row,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use async_trait::async_trait;
|
||||
use futures_util::future::BoxFuture;
|
||||
use sqlx::{PgPool, Row};
|
||||
use futures_util::{future::BoxFuture, stream::TryStream, TryStreamExt};
|
||||
use sqlx::{postgres::PgRow, PgPool, Row};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::{
|
||||
@@ -306,12 +306,13 @@ impl SqlxRequestCandidateReadRepository {
|
||||
&self,
|
||||
request_id: &str,
|
||||
) -> Result<Vec<StoredRequestCandidate>, DataLayerError> {
|
||||
let rows = sqlx::query(LIST_BY_REQUEST_ID_SQL)
|
||||
.bind(request_id)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
rows.iter().map(map_request_candidate_row).collect()
|
||||
collect_query_rows(
|
||||
sqlx::query(LIST_BY_REQUEST_ID_SQL)
|
||||
.bind(request_id)
|
||||
.fetch(&self.pool),
|
||||
map_request_candidate_row,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn list_recent(
|
||||
@@ -322,16 +323,17 @@ impl SqlxRequestCandidateReadRepository {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let rows = sqlx::query(LIST_RECENT_SQL)
|
||||
.bind(i64::try_from(limit).map_err(|_| {
|
||||
DataLayerError::UnexpectedValue(format!(
|
||||
"invalid recent request candidate limit: {limit}"
|
||||
))
|
||||
})?)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
rows.iter().map(map_request_candidate_row).collect()
|
||||
collect_query_rows(
|
||||
sqlx::query(LIST_RECENT_SQL)
|
||||
.bind(i64::try_from(limit).map_err(|_| {
|
||||
DataLayerError::UnexpectedValue(format!(
|
||||
"invalid recent request candidate limit: {limit}"
|
||||
))
|
||||
})?)
|
||||
.fetch(&self.pool),
|
||||
map_request_candidate_row,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn list_by_provider_id(
|
||||
@@ -349,13 +351,14 @@ impl SqlxRequestCandidateReadRepository {
|
||||
))
|
||||
})?;
|
||||
|
||||
let rows = sqlx::query(LIST_BY_PROVIDER_ID_SQL)
|
||||
.bind(provider_id)
|
||||
.bind(limit_value)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
rows.iter().map(map_request_candidate_row).collect()
|
||||
collect_query_rows(
|
||||
sqlx::query(LIST_BY_PROVIDER_ID_SQL)
|
||||
.bind(provider_id)
|
||||
.bind(limit_value)
|
||||
.fetch(&self.pool),
|
||||
map_request_candidate_row,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn list_finalized_by_endpoint_ids_since(
|
||||
@@ -368,18 +371,19 @@ impl SqlxRequestCandidateReadRepository {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let rows = sqlx::query(LIST_FINALIZED_BY_ENDPOINT_IDS_SINCE_SQL)
|
||||
.bind(endpoint_ids)
|
||||
.bind(since_unix_secs as f64)
|
||||
.bind(i64::try_from(limit).map_err(|_| {
|
||||
DataLayerError::UnexpectedValue(format!(
|
||||
"invalid finalized request candidate limit: {limit}"
|
||||
))
|
||||
})?)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
rows.iter().map(map_request_candidate_row).collect()
|
||||
collect_query_rows(
|
||||
sqlx::query(LIST_FINALIZED_BY_ENDPOINT_IDS_SINCE_SQL)
|
||||
.bind(endpoint_ids)
|
||||
.bind(since_unix_secs as f64)
|
||||
.bind(i64::try_from(limit).map_err(|_| {
|
||||
DataLayerError::UnexpectedValue(format!(
|
||||
"invalid finalized request candidate limit: {limit}"
|
||||
))
|
||||
})?)
|
||||
.fetch(&self.pool),
|
||||
map_request_candidate_row,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn count_finalized_statuses_by_endpoint_ids_since(
|
||||
@@ -391,29 +395,29 @@ impl SqlxRequestCandidateReadRepository {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let rows = sqlx::query(COUNT_FINALIZED_STATUSES_BY_ENDPOINT_IDS_SINCE_SQL)
|
||||
let mut rows = sqlx::query(COUNT_FINALIZED_STATUSES_BY_ENDPOINT_IDS_SINCE_SQL)
|
||||
.bind(endpoint_ids)
|
||||
.bind(since_unix_secs as f64)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
|
||||
rows.iter()
|
||||
.map(|row| {
|
||||
.fetch(&self.pool);
|
||||
let mut counts = Vec::new();
|
||||
while let Some(row) = rows.try_next().await.map_postgres_err()? {
|
||||
let entry = {
|
||||
let status = RequestCandidateStatus::from_database(
|
||||
row_get::<String>(row, "status")?.as_str(),
|
||||
row_get::<String>(&row, "status")?.as_str(),
|
||||
)?;
|
||||
Ok(PublicHealthStatusCount {
|
||||
endpoint_id: row_get(row, "endpoint_id")?,
|
||||
PublicHealthStatusCount {
|
||||
endpoint_id: row_get(&row, "endpoint_id")?,
|
||||
status,
|
||||
count: u64::try_from(row_get::<i64>(row, "count")?).map_err(|_| {
|
||||
count: u64::try_from(row_get::<i64>(&row, "count")?).map_err(|_| {
|
||||
DataLayerError::UnexpectedValue(
|
||||
"public health status count out of range".to_string(),
|
||||
)
|
||||
})?,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
};
|
||||
counts.push(entry);
|
||||
}
|
||||
Ok(counts)
|
||||
}
|
||||
|
||||
pub async fn aggregate_finalized_timeline_by_endpoint_ids_since(
|
||||
@@ -434,18 +438,16 @@ impl SqlxRequestCandidateReadRepository {
|
||||
(span_seconds as f64) / (segments as f64)
|
||||
};
|
||||
|
||||
let rows = sqlx::query(AGGREGATE_FINALIZED_TIMELINE_BY_ENDPOINT_IDS_SINCE_SQL)
|
||||
let mut rows = sqlx::query(AGGREGATE_FINALIZED_TIMELINE_BY_ENDPOINT_IDS_SINCE_SQL)
|
||||
.bind(endpoint_ids)
|
||||
.bind(since_unix_secs as f64)
|
||||
.bind(until_unix_secs as f64)
|
||||
.bind(segment_seconds)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
|
||||
rows.iter()
|
||||
.map(|row| {
|
||||
let raw_segment_idx = row_get::<i64>(row, "segment_idx")?;
|
||||
.fetch(&self.pool);
|
||||
let mut buckets = Vec::new();
|
||||
while let Some(row) = rows.try_next().await.map_postgres_err()? {
|
||||
let bucket = {
|
||||
let raw_segment_idx = row_get::<i64>(&row, "segment_idx")?;
|
||||
let segment_idx = if raw_segment_idx < 0 {
|
||||
0
|
||||
} else {
|
||||
@@ -457,31 +459,31 @@ impl SqlxRequestCandidateReadRepository {
|
||||
}
|
||||
.min(segments.saturating_sub(1));
|
||||
|
||||
Ok(PublicHealthTimelineBucket {
|
||||
endpoint_id: row_get(row, "endpoint_id")?,
|
||||
PublicHealthTimelineBucket {
|
||||
endpoint_id: row_get(&row, "endpoint_id")?,
|
||||
segment_idx,
|
||||
total_count: u64::try_from(row_get::<i64>(row, "total_count")?).map_err(
|
||||
total_count: u64::try_from(row_get::<i64>(&row, "total_count")?).map_err(
|
||||
|_| {
|
||||
DataLayerError::UnexpectedValue(
|
||||
"public health total_count out of range".to_string(),
|
||||
)
|
||||
},
|
||||
)?,
|
||||
success_count: u64::try_from(row_get::<i64>(row, "success_count")?).map_err(
|
||||
success_count: u64::try_from(row_get::<i64>(&row, "success_count")?).map_err(
|
||||
|_| {
|
||||
DataLayerError::UnexpectedValue(
|
||||
"public health success_count out of range".to_string(),
|
||||
)
|
||||
},
|
||||
)?,
|
||||
failed_count: u64::try_from(row_get::<i64>(row, "failed_count")?).map_err(
|
||||
failed_count: u64::try_from(row_get::<i64>(&row, "failed_count")?).map_err(
|
||||
|_| {
|
||||
DataLayerError::UnexpectedValue(
|
||||
"public health failed_count out of range".to_string(),
|
||||
)
|
||||
},
|
||||
)?,
|
||||
min_created_at_unix_ms: row_get::<Option<i64>>(row, "min_created_at_unix_ms")?
|
||||
min_created_at_unix_ms: row_get::<Option<i64>>(&row, "min_created_at_unix_ms")?
|
||||
.map(|value| {
|
||||
u64::try_from(value).map_err(|_| {
|
||||
DataLayerError::UnexpectedValue(format!(
|
||||
@@ -490,7 +492,7 @@ impl SqlxRequestCandidateReadRepository {
|
||||
})
|
||||
})
|
||||
.transpose()?,
|
||||
max_created_at_unix_ms: row_get::<Option<i64>>(row, "max_created_at_unix_ms")?
|
||||
max_created_at_unix_ms: row_get::<Option<i64>>(&row, "max_created_at_unix_ms")?
|
||||
.map(|value| {
|
||||
u64::try_from(value).map_err(|_| {
|
||||
DataLayerError::UnexpectedValue(format!(
|
||||
@@ -499,9 +501,11 @@ impl SqlxRequestCandidateReadRepository {
|
||||
})
|
||||
})
|
||||
.transpose()?,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
};
|
||||
buckets.push(bucket);
|
||||
}
|
||||
Ok(buckets)
|
||||
}
|
||||
|
||||
pub async fn upsert(
|
||||
@@ -651,9 +655,21 @@ impl RequestCandidateWriteRepository for SqlxRequestCandidateReadRepository {
|
||||
}
|
||||
}
|
||||
|
||||
fn map_request_candidate_row(
|
||||
row: &sqlx::postgres::PgRow,
|
||||
) -> Result<StoredRequestCandidate, DataLayerError> {
|
||||
async fn collect_query_rows<T, S>(
|
||||
mut rows: S,
|
||||
map_row: fn(&PgRow) -> Result<T, DataLayerError>,
|
||||
) -> Result<Vec<T>, DataLayerError>
|
||||
where
|
||||
S: TryStream<Ok = PgRow, Error = sqlx::Error> + Unpin,
|
||||
{
|
||||
let mut items = Vec::new();
|
||||
while let Some(row) = rows.try_next().await.map_postgres_err()? {
|
||||
items.push(map_row(&row)?);
|
||||
}
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
fn map_request_candidate_row(row: &PgRow) -> Result<StoredRequestCandidate, DataLayerError> {
|
||||
let status = RequestCandidateStatus::from_database(row_get::<String>(row, "status")?.as_str())?;
|
||||
StoredRequestCandidate::new(
|
||||
row_get(row, "id")?,
|
||||
@@ -683,7 +699,7 @@ fn map_request_candidate_row(
|
||||
)
|
||||
}
|
||||
|
||||
fn row_get<T>(row: &sqlx::postgres::PgRow, column: &str) -> Result<T, DataLayerError>
|
||||
fn row_get<T>(row: &PgRow, column: &str) -> Result<T, DataLayerError>
|
||||
where
|
||||
for<'r> T: sqlx::Decode<'r, sqlx::Postgres> + sqlx::Type<sqlx::Postgres>,
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use async_trait::async_trait;
|
||||
use futures_util::TryStreamExt;
|
||||
use sqlx::{postgres::PgRow, PgPool, Postgres, QueryBuilder, Row};
|
||||
|
||||
use super::types::{
|
||||
@@ -91,16 +92,15 @@ WHERE file_name = $1
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
let rows = build_list_rows_query(query)
|
||||
.build()
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
let mut builder = build_list_rows_query(query);
|
||||
let built_query = builder.build();
|
||||
let mut rows = built_query.fetch(&self.pool);
|
||||
let mut items = Vec::new();
|
||||
while let Some(row) = rows.try_next().await.map_postgres_err()? {
|
||||
items.push(Self::map_row(&row)?);
|
||||
}
|
||||
Ok(StoredGeminiFileMappingListPage {
|
||||
items: rows
|
||||
.iter()
|
||||
.map(Self::map_row)
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
items,
|
||||
total: usize::try_from(total).unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
@@ -133,7 +133,7 @@ FROM gemini_file_mappings
|
||||
.map_postgres_err()?,
|
||||
)
|
||||
.unwrap_or_default();
|
||||
let by_mime_type_rows = sqlx::query(
|
||||
let mut by_mime_type_rows = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
COALESCE(NULLIF(TRIM(mime_type), ''), 'unknown') AS mime_type,
|
||||
@@ -145,23 +145,20 @@ ORDER BY mime_type ASC
|
||||
"#,
|
||||
)
|
||||
.bind(now_unix_secs as f64)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
.fetch(&self.pool);
|
||||
let mut by_mime_type = Vec::new();
|
||||
while let Some(row) = by_mime_type_rows.try_next().await.map_postgres_err()? {
|
||||
by_mime_type.push(GeminiFileMappingMimeTypeCount {
|
||||
mime_type: row.try_get("mime_type").map_postgres_err()?,
|
||||
count: usize::try_from(row.try_get::<i64, _>("count").map_postgres_err()?)
|
||||
.unwrap_or_default(),
|
||||
});
|
||||
}
|
||||
Ok(GeminiFileMappingStats {
|
||||
total_mappings,
|
||||
active_mappings,
|
||||
expired_mappings: total_mappings.saturating_sub(active_mappings),
|
||||
by_mime_type: by_mime_type_rows
|
||||
.into_iter()
|
||||
.map(|row| {
|
||||
Ok(GeminiFileMappingMimeTypeCount {
|
||||
mime_type: row.try_get("mime_type").map_postgres_err()?,
|
||||
count: usize::try_from(row.try_get::<i64, _>("count").map_postgres_err()?)
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, DataLayerError>>()?,
|
||||
by_mime_type,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use async_trait::async_trait;
|
||||
use futures_util::{stream::TryStream, TryStreamExt};
|
||||
use serde_json::Value;
|
||||
use sqlx::{postgres::PgRow, PgPool, Postgres, QueryBuilder, Row};
|
||||
|
||||
@@ -180,12 +181,8 @@ impl SqlxGlobalModelReadRepository {
|
||||
.push_bind(query.offset as i64)
|
||||
.push(" LIMIT ")
|
||||
.push_bind(query.limit as i64);
|
||||
let rows = list_builder
|
||||
.build()
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
let items = rows.iter().map(map_row).collect::<Result<Vec<_>, _>>()?;
|
||||
let query = list_builder.build();
|
||||
let items = collect_query_rows(query.fetch(&self.pool), map_row).await?;
|
||||
|
||||
Ok(StoredPublicGlobalModelPage { items, total })
|
||||
}
|
||||
@@ -198,17 +195,13 @@ impl SqlxGlobalModelReadRepository {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let rows = build_provider_id_list_query(
|
||||
let mut builder = build_provider_id_list_query(
|
||||
LIST_PROVIDER_MODEL_STATS_PREFIX,
|
||||
provider_ids,
|
||||
")\nGROUP BY provider_id\nORDER BY provider_id ASC",
|
||||
)
|
||||
.build()
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
|
||||
rows.iter().map(map_provider_model_stats_row).collect()
|
||||
);
|
||||
let query = builder.build();
|
||||
collect_query_rows(query.fetch(&self.pool), map_provider_model_stats_row).await
|
||||
}
|
||||
|
||||
pub async fn list_active_global_model_ids_by_provider_ids(
|
||||
@@ -219,19 +212,17 @@ impl SqlxGlobalModelReadRepository {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let rows = build_provider_id_list_query(
|
||||
let mut builder = build_provider_id_list_query(
|
||||
LIST_ACTIVE_GLOBAL_MODEL_IDS_BY_PROVIDER_IDS_PREFIX,
|
||||
provider_ids,
|
||||
")\nAND is_active = TRUE\nAND global_model_id IS NOT NULL\nORDER BY provider_id ASC, global_model_id ASC",
|
||||
);
|
||||
let query = builder.build();
|
||||
collect_query_rows(
|
||||
query.fetch(&self.pool),
|
||||
map_provider_active_global_model_row,
|
||||
)
|
||||
.build()
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
|
||||
rows.iter()
|
||||
.map(map_provider_active_global_model_row)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn list_admin_provider_models(
|
||||
@@ -250,12 +241,8 @@ impl SqlxGlobalModelReadRepository {
|
||||
.push_bind(query.offset as i64)
|
||||
.push(" LIMIT ")
|
||||
.push_bind(query.limit as i64);
|
||||
let rows = builder
|
||||
.build()
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
rows.iter().map(map_admin_provider_model_row).collect()
|
||||
let query = builder.build();
|
||||
collect_query_rows(query.fetch(&self.pool), map_admin_provider_model_row).await
|
||||
}
|
||||
|
||||
pub async fn list_admin_global_models(
|
||||
@@ -281,15 +268,8 @@ impl SqlxGlobalModelReadRepository {
|
||||
.push_bind(query.offset as i64)
|
||||
.push(" LIMIT ")
|
||||
.push_bind(query.limit as i64);
|
||||
let rows = list_builder
|
||||
.build()
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
let items = rows
|
||||
.iter()
|
||||
.map(map_admin_global_model_row)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let query = list_builder.build();
|
||||
let items = collect_query_rows(query.fetch(&self.pool), map_admin_global_model_row).await?;
|
||||
Ok(StoredAdminGlobalModelPage { items, total })
|
||||
}
|
||||
|
||||
@@ -343,8 +323,9 @@ LIMIT 1
|
||||
&self,
|
||||
provider_id: &str,
|
||||
) -> Result<Vec<StoredAdminProviderModel>, DataLayerError> {
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
collect_query_rows(
|
||||
sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
m.id,
|
||||
m.provider_id,
|
||||
@@ -375,13 +356,12 @@ WHERE m.provider_id = $1
|
||||
AND gm.is_active = TRUE
|
||||
ORDER BY gm.name ASC, m.created_at DESC, m.id ASC
|
||||
"#,
|
||||
)
|
||||
.bind(provider_id)
|
||||
.fetch(&self.pool),
|
||||
map_admin_provider_model_row,
|
||||
)
|
||||
.bind(provider_id)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
|
||||
rows.iter().map(map_admin_provider_model_row).collect()
|
||||
}
|
||||
|
||||
pub async fn get_admin_global_model_by_id(
|
||||
@@ -482,8 +462,9 @@ LIMIT 1
|
||||
&self,
|
||||
global_model_id: &str,
|
||||
) -> Result<Vec<StoredAdminProviderModel>, DataLayerError> {
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
collect_query_rows(
|
||||
sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
m.id,
|
||||
m.provider_id,
|
||||
@@ -512,13 +493,12 @@ LEFT JOIN global_models gm ON gm.id = m.global_model_id
|
||||
WHERE m.global_model_id = $1
|
||||
ORDER BY m.created_at DESC, m.id ASC
|
||||
"#,
|
||||
)
|
||||
.bind(global_model_id)
|
||||
.fetch(&self.pool),
|
||||
map_admin_provider_model_row,
|
||||
)
|
||||
.bind(global_model_id)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
|
||||
rows.iter().map(map_admin_provider_model_row).collect()
|
||||
}
|
||||
|
||||
pub async fn create_admin_provider_model(
|
||||
@@ -801,12 +781,8 @@ LIMIT 1
|
||||
.push_bind(query.offset as i64)
|
||||
.push(" LIMIT ")
|
||||
.push_bind(query.limit as i64);
|
||||
let rows = builder
|
||||
.build()
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
rows.iter().map(map_public_catalog_model_row).collect()
|
||||
let query = builder.build();
|
||||
collect_query_rows(query.fetch(&self.pool), map_public_catalog_model_row).await
|
||||
}
|
||||
|
||||
async fn search_public_catalog_models(
|
||||
@@ -822,12 +798,8 @@ LIMIT 1
|
||||
builder
|
||||
.push(" ORDER BY p.provider_priority ASC, p.name ASC, COALESCE(gm.name, m.provider_model_name) ASC, m.id ASC LIMIT ")
|
||||
.push_bind(query.limit as i64);
|
||||
let rows = builder
|
||||
.build()
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
rows.iter().map(map_public_catalog_model_row).collect()
|
||||
let query = builder.build();
|
||||
collect_query_rows(query.fetch(&self.pool), map_public_catalog_model_row).await
|
||||
}
|
||||
|
||||
async fn list_admin_global_models(
|
||||
@@ -1012,6 +984,20 @@ fn map_row(row: &PgRow) -> Result<StoredPublicGlobalModel, DataLayerError> {
|
||||
)
|
||||
}
|
||||
|
||||
async fn collect_query_rows<T, S>(
|
||||
mut rows: S,
|
||||
map_row: fn(&PgRow) -> Result<T, DataLayerError>,
|
||||
) -> Result<Vec<T>, DataLayerError>
|
||||
where
|
||||
S: TryStream<Ok = PgRow, Error = sqlx::Error> + Unpin,
|
||||
{
|
||||
let mut items = Vec::new();
|
||||
while let Some(row) = rows.try_next().await.map_postgres_err()? {
|
||||
items.push(map_row(&row)?);
|
||||
}
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
fn apply_public_catalog_model_filters(
|
||||
builder: &mut QueryBuilder<'_, Postgres>,
|
||||
provider_id: Option<&str>,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use async_trait::async_trait;
|
||||
use futures_util::TryStreamExt;
|
||||
use sqlx::{postgres::PgRow, PgPool, Row};
|
||||
|
||||
use super::types::{
|
||||
@@ -269,20 +270,19 @@ impl ManagementTokenReadRepository for SqlxManagementTokenRepository {
|
||||
.map_postgres_err()?;
|
||||
let total = count_row.try_get::<i64, _>("total").map_postgres_err()?;
|
||||
|
||||
let rows = sqlx::query(LIST_MANAGEMENT_TOKENS_SQL)
|
||||
let mut rows = sqlx::query(LIST_MANAGEMENT_TOKENS_SQL)
|
||||
.bind(query.user_id.as_deref())
|
||||
.bind(query.is_active)
|
||||
.bind(i64::try_from(query.offset).unwrap_or(i64::MAX))
|
||||
.bind(i64::try_from(query.limit).unwrap_or(i64::MAX))
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
.fetch(&self.pool);
|
||||
let mut items = Vec::new();
|
||||
while let Some(row) = rows.try_next().await.map_postgres_err()? {
|
||||
items.push(map_token_with_user_row(&row)?);
|
||||
}
|
||||
|
||||
Ok(StoredManagementTokenListPage {
|
||||
items: rows
|
||||
.iter()
|
||||
.map(map_token_with_user_row)
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
items,
|
||||
total: usize::try_from(total.max(0)).unwrap_or(usize::MAX),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use async_trait::async_trait;
|
||||
use futures_util::TryStreamExt;
|
||||
use sqlx::{postgres::PgRow, PgPool, Row};
|
||||
|
||||
use super::types::{
|
||||
@@ -181,11 +182,12 @@ impl OAuthProviderReadRepository for SqlxOAuthProviderRepository {
|
||||
async fn list_oauth_provider_configs(
|
||||
&self,
|
||||
) -> Result<Vec<StoredOAuthProviderConfig>, DataLayerError> {
|
||||
let rows = sqlx::query(LIST_OAUTH_PROVIDER_CONFIGS_SQL)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
rows.iter().map(map_oauth_provider_row).collect()
|
||||
let mut rows = sqlx::query(LIST_OAUTH_PROVIDER_CONFIGS_SQL).fetch(&self.pool);
|
||||
let mut items = Vec::new();
|
||||
while let Some(row) = rows.try_next().await.map_postgres_err()? {
|
||||
items.push(map_oauth_provider_row(&row)?);
|
||||
}
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
async fn get_oauth_provider_config(
|
||||
|
||||
@@ -4,9 +4,9 @@ use std::sync::RwLock;
|
||||
use async_trait::async_trait;
|
||||
|
||||
use super::{
|
||||
ProviderCatalogKeyListQuery, ProviderCatalogReadRepository, ProviderCatalogWriteRepository,
|
||||
StoredProviderCatalogEndpoint, StoredProviderCatalogKey, StoredProviderCatalogKeyPage,
|
||||
StoredProviderCatalogKeyStats, StoredProviderCatalogProvider,
|
||||
ProviderCatalogKeyListOrder, ProviderCatalogKeyListQuery, ProviderCatalogReadRepository,
|
||||
ProviderCatalogWriteRepository, StoredProviderCatalogEndpoint, StoredProviderCatalogKey,
|
||||
StoredProviderCatalogKeyPage, StoredProviderCatalogKeyStats, StoredProviderCatalogProvider,
|
||||
};
|
||||
use crate::DataLayerError;
|
||||
|
||||
@@ -146,6 +146,13 @@ impl ProviderCatalogReadRepository for InMemoryProviderCatalogReadRepository {
|
||||
Ok(keys)
|
||||
}
|
||||
|
||||
async fn list_key_summaries_by_provider_ids(
|
||||
&self,
|
||||
provider_ids: &[String],
|
||||
) -> Result<Vec<StoredProviderCatalogKey>, DataLayerError> {
|
||||
Self::list_keys_by_provider_ids(self, provider_ids).await
|
||||
}
|
||||
|
||||
async fn list_keys_page(
|
||||
&self,
|
||||
query: &ProviderCatalogKeyListQuery,
|
||||
@@ -170,12 +177,28 @@ impl ProviderCatalogReadRepository for InMemoryProviderCatalogReadRepository {
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
keys.sort_by(|left, right| {
|
||||
left.internal_priority
|
||||
.cmp(&right.internal_priority)
|
||||
.then(left.name.cmp(&right.name))
|
||||
.then(left.id.cmp(&right.id))
|
||||
});
|
||||
match query.order {
|
||||
ProviderCatalogKeyListOrder::Name => {
|
||||
keys.sort_by(|left, right| {
|
||||
left.internal_priority
|
||||
.cmp(&right.internal_priority)
|
||||
.then(left.name.cmp(&right.name))
|
||||
.then(left.id.cmp(&right.id))
|
||||
});
|
||||
}
|
||||
ProviderCatalogKeyListOrder::CreatedAt => {
|
||||
keys.sort_by(|left, right| {
|
||||
left.internal_priority
|
||||
.cmp(&right.internal_priority)
|
||||
.then(
|
||||
left.created_at_unix_ms
|
||||
.unwrap_or_default()
|
||||
.cmp(&right.created_at_unix_ms.unwrap_or_default()),
|
||||
)
|
||||
.then(left.id.cmp(&right.id))
|
||||
});
|
||||
}
|
||||
}
|
||||
let total = keys.len();
|
||||
let items = keys
|
||||
.into_iter()
|
||||
@@ -415,8 +438,9 @@ impl ProviderCatalogWriteRepository for InMemoryProviderCatalogReadRepository {
|
||||
mod tests {
|
||||
use super::InMemoryProviderCatalogReadRepository;
|
||||
use crate::repository::provider_catalog::{
|
||||
ProviderCatalogKeyListQuery, ProviderCatalogReadRepository, ProviderCatalogWriteRepository,
|
||||
StoredProviderCatalogEndpoint, StoredProviderCatalogKey, StoredProviderCatalogProvider,
|
||||
ProviderCatalogKeyListOrder, ProviderCatalogKeyListQuery, ProviderCatalogReadRepository,
|
||||
ProviderCatalogWriteRepository, StoredProviderCatalogEndpoint, StoredProviderCatalogKey,
|
||||
StoredProviderCatalogProvider,
|
||||
};
|
||||
|
||||
fn sample_provider(id: &str) -> StoredProviderCatalogProvider {
|
||||
@@ -583,6 +607,7 @@ mod tests {
|
||||
is_active: Some(true),
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
order: ProviderCatalogKeyListOrder::Name,
|
||||
})
|
||||
.await
|
||||
.expect("keys should page");
|
||||
@@ -598,6 +623,44 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn paginates_provider_keys_by_created_at_when_requested() {
|
||||
let mut early = sample_key("key-1", "provider-1");
|
||||
early.name = "zeta".to_string();
|
||||
early.internal_priority = 10;
|
||||
early.created_at_unix_ms = Some(10);
|
||||
let mut late = sample_key("key-2", "provider-1");
|
||||
late.name = "alpha".to_string();
|
||||
late.internal_priority = 10;
|
||||
late.created_at_unix_ms = Some(20);
|
||||
let repository = InMemoryProviderCatalogReadRepository::seed(
|
||||
vec![sample_provider("provider-1")],
|
||||
vec![],
|
||||
vec![late, early],
|
||||
);
|
||||
|
||||
let page = repository
|
||||
.list_keys_page(&ProviderCatalogKeyListQuery {
|
||||
provider_id: "provider-1".to_string(),
|
||||
search: None,
|
||||
is_active: None,
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
order: ProviderCatalogKeyListOrder::CreatedAt,
|
||||
})
|
||||
.await
|
||||
.expect("keys should page");
|
||||
|
||||
assert_eq!(page.total, 2);
|
||||
assert_eq!(
|
||||
page.items
|
||||
.iter()
|
||||
.map(|item| item.id.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["key-1", "key-2"]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn summarizes_provider_key_stats() {
|
||||
let mut inactive = sample_key("key-2", "provider-1");
|
||||
|
||||
@@ -3,9 +3,9 @@ mod sql;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
pub(crate) use aether_data_contracts::repository::provider_catalog::{
|
||||
ProviderCatalogKeyListQuery, ProviderCatalogReadRepository, ProviderCatalogWriteRepository,
|
||||
StoredProviderCatalogEndpoint, StoredProviderCatalogKey, StoredProviderCatalogKeyPage,
|
||||
StoredProviderCatalogKeyStats, StoredProviderCatalogProvider,
|
||||
ProviderCatalogKeyListOrder, ProviderCatalogKeyListQuery, ProviderCatalogReadRepository,
|
||||
ProviderCatalogWriteRepository, StoredProviderCatalogEndpoint, StoredProviderCatalogKey,
|
||||
StoredProviderCatalogKeyPage, StoredProviderCatalogKeyStats, StoredProviderCatalogProvider,
|
||||
};
|
||||
pub use memory::InMemoryProviderCatalogReadRepository;
|
||||
pub use sql::SqlxProviderCatalogReadRepository;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use async_trait::async_trait;
|
||||
use futures_util::TryStreamExt;
|
||||
use sqlx::{postgres::PgRow, PgPool, Postgres, QueryBuilder, Row};
|
||||
|
||||
use super::{
|
||||
ProviderCatalogKeyListQuery, ProviderCatalogReadRepository, ProviderCatalogWriteRepository,
|
||||
StoredProviderCatalogEndpoint, StoredProviderCatalogKey, StoredProviderCatalogKeyPage,
|
||||
StoredProviderCatalogKeyStats, StoredProviderCatalogProvider,
|
||||
ProviderCatalogKeyListOrder, ProviderCatalogKeyListQuery, ProviderCatalogReadRepository,
|
||||
ProviderCatalogWriteRepository, StoredProviderCatalogEndpoint, StoredProviderCatalogKey,
|
||||
StoredProviderCatalogKeyPage, StoredProviderCatalogKeyStats, StoredProviderCatalogProvider,
|
||||
};
|
||||
use crate::{
|
||||
error::{postgres_error, SqlxResultExt},
|
||||
@@ -240,6 +241,61 @@ FROM provider_api_keys
|
||||
WHERE provider_id IN (
|
||||
"#;
|
||||
|
||||
const LIST_KEY_SUMMARIES_BY_PROVIDER_IDS_PREFIX: &str = r#"
|
||||
SELECT
|
||||
id,
|
||||
provider_id,
|
||||
COALESCE(NULLIF(name, ''), id) AS name,
|
||||
COALESCE(NULLIF(auth_type, ''), 'summary') AS auth_type,
|
||||
capabilities,
|
||||
is_active,
|
||||
api_formats,
|
||||
'summary' AS api_key,
|
||||
NULL::text AS auth_config,
|
||||
NULL::text AS note,
|
||||
internal_priority,
|
||||
rate_multipliers,
|
||||
global_priority_by_format,
|
||||
NULL::jsonb AS allowed_models,
|
||||
NULL::bigint AS expires_at_unix_secs,
|
||||
cache_ttl_minutes,
|
||||
max_probe_interval_minutes,
|
||||
NULL::jsonb AS proxy,
|
||||
NULL::jsonb AS fingerprint,
|
||||
NULL::integer AS rpm_limit,
|
||||
NULL::integer AS learned_rpm_limit,
|
||||
NULL::integer AS concurrent_429_count,
|
||||
NULL::integer AS rpm_429_count,
|
||||
NULL::bigint AS last_429_at_unix_secs,
|
||||
NULL::text AS last_429_type,
|
||||
NULL::jsonb AS adjustment_history,
|
||||
NULL::jsonb AS utilization_samples,
|
||||
NULL::bigint AS last_probe_increase_at_unix_secs,
|
||||
request_count,
|
||||
0::bigint AS total_tokens,
|
||||
0::double precision AS total_cost_usd,
|
||||
success_count,
|
||||
NULL::integer AS error_count,
|
||||
total_response_time_ms,
|
||||
EXTRACT(EPOCH FROM last_used_at)::bigint AS last_used_at_unix_secs,
|
||||
auto_fetch_models,
|
||||
NULL::bigint AS last_models_fetch_at_unix_secs,
|
||||
NULL::text AS last_models_fetch_error,
|
||||
NULL::jsonb AS locked_models,
|
||||
NULL::jsonb AS model_include_patterns,
|
||||
NULL::jsonb AS model_exclude_patterns,
|
||||
NULL::jsonb AS upstream_metadata,
|
||||
EXTRACT(EPOCH FROM oauth_invalid_at)::bigint AS oauth_invalid_at_unix_secs,
|
||||
oauth_invalid_reason,
|
||||
NULL::jsonb AS status_snapshot,
|
||||
EXTRACT(EPOCH FROM created_at)::bigint AS created_at_unix_ms,
|
||||
EXTRACT(EPOCH FROM updated_at)::bigint AS updated_at_unix_secs,
|
||||
health_by_format,
|
||||
circuit_breaker_by_format
|
||||
FROM provider_api_keys
|
||||
WHERE provider_id IN (
|
||||
"#;
|
||||
|
||||
const LIST_KEY_STATS_BY_PROVIDER_IDS_PREFIX: &str = r#"
|
||||
SELECT
|
||||
provider_id,
|
||||
@@ -271,24 +327,26 @@ impl SqlxProviderCatalogReadRepository {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let rows = build_list_query(
|
||||
LIST_PROVIDERS_BY_IDS_PREFIX,
|
||||
provider_ids,
|
||||
" ORDER BY name ASC",
|
||||
collect_query_rows(
|
||||
build_list_query(
|
||||
LIST_PROVIDERS_BY_IDS_PREFIX,
|
||||
provider_ids,
|
||||
" ORDER BY name ASC",
|
||||
)
|
||||
.build()
|
||||
.fetch(&self.pool),
|
||||
map_provider_row,
|
||||
)
|
||||
.build()
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
rows.iter().map(map_provider_row).collect()
|
||||
}
|
||||
|
||||
pub async fn list_providers(
|
||||
&self,
|
||||
active_only: bool,
|
||||
) -> Result<Vec<StoredProviderCatalogProvider>, DataLayerError> {
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
collect_query_rows(
|
||||
sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
@@ -317,12 +375,12 @@ FROM providers
|
||||
WHERE ($1::boolean = false OR is_active = true)
|
||||
ORDER BY provider_priority ASC, name ASC
|
||||
"#,
|
||||
)
|
||||
.bind(active_only)
|
||||
.fetch(&self.pool),
|
||||
map_provider_row,
|
||||
)
|
||||
.bind(active_only)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
rows.iter().map(map_provider_row).collect()
|
||||
}
|
||||
|
||||
pub async fn list_endpoints_by_ids(
|
||||
@@ -333,28 +391,35 @@ ORDER BY provider_priority ASC, name ASC
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let rows = match build_list_query(
|
||||
LIST_ENDPOINTS_BY_IDS_PREFIX,
|
||||
endpoint_ids,
|
||||
" ORDER BY api_format ASC, id ASC",
|
||||
)
|
||||
.build()
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
{
|
||||
Ok(rows) => rows,
|
||||
Err(error) if is_missing_endpoint_health_score_column(&error) => build_list_query(
|
||||
LIST_ENDPOINTS_BY_IDS_PREFIX_LEGACY,
|
||||
let rows = match collect_query_rows(
|
||||
build_list_query(
|
||||
LIST_ENDPOINTS_BY_IDS_PREFIX,
|
||||
endpoint_ids,
|
||||
" ORDER BY api_format ASC, id ASC",
|
||||
)
|
||||
.build()
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?,
|
||||
.fetch(&self.pool),
|
||||
map_endpoint_row,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(rows) => rows,
|
||||
Err(error) if is_missing_endpoint_health_score_column_sql(&error) => {
|
||||
collect_query_rows(
|
||||
build_list_query(
|
||||
LIST_ENDPOINTS_BY_IDS_PREFIX_LEGACY,
|
||||
endpoint_ids,
|
||||
" ORDER BY api_format ASC, id ASC",
|
||||
)
|
||||
.build()
|
||||
.fetch(&self.pool),
|
||||
map_endpoint_row,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
Err(error) => return Err(postgres_error(error)),
|
||||
};
|
||||
rows.iter().map(map_endpoint_row).collect()
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
pub async fn list_endpoints_by_provider_ids(
|
||||
@@ -365,28 +430,35 @@ ORDER BY provider_priority ASC, name ASC
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let rows = match build_list_query(
|
||||
LIST_ENDPOINTS_BY_PROVIDER_IDS_PREFIX,
|
||||
provider_ids,
|
||||
" ORDER BY provider_id ASC, api_format ASC, id ASC",
|
||||
)
|
||||
.build()
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
{
|
||||
Ok(rows) => rows,
|
||||
Err(error) if is_missing_endpoint_health_score_column(&error) => build_list_query(
|
||||
LIST_ENDPOINTS_BY_PROVIDER_IDS_PREFIX_LEGACY,
|
||||
let rows = match collect_query_rows(
|
||||
build_list_query(
|
||||
LIST_ENDPOINTS_BY_PROVIDER_IDS_PREFIX,
|
||||
provider_ids,
|
||||
" ORDER BY provider_id ASC, api_format ASC, id ASC",
|
||||
)
|
||||
.build()
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?,
|
||||
.fetch(&self.pool),
|
||||
map_endpoint_row,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(rows) => rows,
|
||||
Err(error) if is_missing_endpoint_health_score_column_sql(&error) => {
|
||||
collect_query_rows(
|
||||
build_list_query(
|
||||
LIST_ENDPOINTS_BY_PROVIDER_IDS_PREFIX_LEGACY,
|
||||
provider_ids,
|
||||
" ORDER BY provider_id ASC, api_format ASC, id ASC",
|
||||
)
|
||||
.build()
|
||||
.fetch(&self.pool),
|
||||
map_endpoint_row,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
Err(error) => return Err(postgres_error(error)),
|
||||
};
|
||||
rows.iter().map(map_endpoint_row).collect()
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
pub async fn list_keys_by_ids(
|
||||
@@ -397,16 +469,17 @@ ORDER BY provider_priority ASC, name ASC
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let rows = build_list_query(
|
||||
LIST_KEYS_BY_IDS_PREFIX,
|
||||
key_ids,
|
||||
" ORDER BY name ASC, id ASC",
|
||||
collect_query_rows(
|
||||
build_list_query(
|
||||
LIST_KEYS_BY_IDS_PREFIX,
|
||||
key_ids,
|
||||
" ORDER BY name ASC, id ASC",
|
||||
)
|
||||
.build()
|
||||
.fetch(&self.pool),
|
||||
map_key_row,
|
||||
)
|
||||
.build()
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
rows.iter().map(map_key_row).collect()
|
||||
}
|
||||
|
||||
pub async fn list_keys_by_provider_ids(
|
||||
@@ -417,16 +490,38 @@ ORDER BY provider_priority ASC, name ASC
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let rows = build_list_query(
|
||||
LIST_KEYS_BY_PROVIDER_IDS_PREFIX,
|
||||
provider_ids,
|
||||
" ORDER BY provider_id ASC, name ASC, id ASC",
|
||||
collect_query_rows(
|
||||
build_list_query(
|
||||
LIST_KEYS_BY_PROVIDER_IDS_PREFIX,
|
||||
provider_ids,
|
||||
" ORDER BY provider_id ASC, name ASC, id ASC",
|
||||
)
|
||||
.build()
|
||||
.fetch(&self.pool),
|
||||
map_key_row,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn list_key_summaries_by_provider_ids(
|
||||
&self,
|
||||
provider_ids: &[String],
|
||||
) -> Result<Vec<StoredProviderCatalogKey>, DataLayerError> {
|
||||
if provider_ids.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
collect_query_rows(
|
||||
build_list_query(
|
||||
LIST_KEY_SUMMARIES_BY_PROVIDER_IDS_PREFIX,
|
||||
provider_ids,
|
||||
" ORDER BY provider_id ASC, id ASC",
|
||||
)
|
||||
.build()
|
||||
.fetch(&self.pool),
|
||||
map_key_row,
|
||||
)
|
||||
.build()
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
rows.iter().map(map_key_row).collect()
|
||||
}
|
||||
|
||||
pub async fn list_keys_page(
|
||||
@@ -457,6 +552,12 @@ ORDER BY provider_priority ASC, name ASC
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(|value| format!("%{}%", value.to_ascii_lowercase()));
|
||||
let order_by = match query.order {
|
||||
ProviderCatalogKeyListOrder::Name => "internal_priority ASC, name ASC, id ASC",
|
||||
ProviderCatalogKeyListOrder::CreatedAt => {
|
||||
"internal_priority ASC, COALESCE(created_at, TO_TIMESTAMP(0)) ASC, id ASC"
|
||||
}
|
||||
};
|
||||
|
||||
let count_row = sqlx::query(
|
||||
r#"
|
||||
@@ -475,7 +576,7 @@ WHERE provider_id = $1
|
||||
.map_postgres_err()?;
|
||||
let total = row_get::<i64>(&count_row, "total")?.max(0) as usize;
|
||||
|
||||
let rows = sqlx::query(
|
||||
let sql = format!(
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
@@ -531,23 +632,22 @@ FROM provider_api_keys
|
||||
WHERE provider_id = $1
|
||||
AND ($2::TEXT IS NULL OR LOWER(name) LIKE $2 OR LOWER(id) LIKE $2)
|
||||
AND ($3::BOOLEAN IS NULL OR is_active = $3)
|
||||
ORDER BY internal_priority ASC, name ASC, id ASC
|
||||
ORDER BY {order_by}
|
||||
OFFSET $4
|
||||
LIMIT $5
|
||||
"#,
|
||||
);
|
||||
let items = collect_query_rows(
|
||||
sqlx::query(&sql)
|
||||
.bind(&query.provider_id)
|
||||
.bind(search_pattern.as_deref())
|
||||
.bind(query.is_active)
|
||||
.bind(offset)
|
||||
.bind(limit)
|
||||
.fetch(&self.pool),
|
||||
map_key_row,
|
||||
)
|
||||
.bind(&query.provider_id)
|
||||
.bind(search_pattern.as_deref())
|
||||
.bind(query.is_active)
|
||||
.bind(offset)
|
||||
.bind(limit)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
let items = rows
|
||||
.iter()
|
||||
.map(map_key_row)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
.await?;
|
||||
|
||||
Ok(StoredProviderCatalogKeyPage { items, total })
|
||||
}
|
||||
@@ -560,16 +660,17 @@ LIMIT $5
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let rows = build_list_query(
|
||||
LIST_KEY_STATS_BY_PROVIDER_IDS_PREFIX,
|
||||
provider_ids,
|
||||
"\nGROUP BY provider_id\nORDER BY provider_id ASC",
|
||||
collect_query_rows(
|
||||
build_list_query(
|
||||
LIST_KEY_STATS_BY_PROVIDER_IDS_PREFIX,
|
||||
provider_ids,
|
||||
"\nGROUP BY provider_id\nORDER BY provider_id ASC",
|
||||
)
|
||||
.build()
|
||||
.fetch(&self.pool),
|
||||
map_key_stats_row,
|
||||
)
|
||||
.build()
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
rows.iter().map(map_key_stats_row).collect()
|
||||
}
|
||||
|
||||
pub async fn update_key_oauth_credentials(
|
||||
@@ -1794,6 +1895,13 @@ impl ProviderCatalogReadRepository for SqlxProviderCatalogReadRepository {
|
||||
Self::list_keys_by_provider_ids(self, provider_ids).await
|
||||
}
|
||||
|
||||
async fn list_key_summaries_by_provider_ids(
|
||||
&self,
|
||||
provider_ids: &[String],
|
||||
) -> Result<Vec<StoredProviderCatalogKey>, DataLayerError> {
|
||||
Self::list_key_summaries_by_provider_ids(self, provider_ids).await
|
||||
}
|
||||
|
||||
async fn list_keys_page(
|
||||
&self,
|
||||
query: &ProviderCatalogKeyListQuery,
|
||||
@@ -2051,6 +2159,29 @@ fn is_missing_endpoint_health_score_column(error: &sqlx::Error) -> bool {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn is_missing_endpoint_health_score_column_sql(error: &DataLayerError) -> bool {
|
||||
match error {
|
||||
DataLayerError::Postgres(message) => {
|
||||
message.contains("endpoint_health_score") && message.contains("does not exist")
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
async fn collect_query_rows<T, S>(
|
||||
mut rows: S,
|
||||
mapper: fn(&PgRow) -> Result<T, DataLayerError>,
|
||||
) -> Result<Vec<T>, DataLayerError>
|
||||
where
|
||||
S: futures_util::TryStream<Ok = PgRow, Error = sqlx::Error> + Unpin,
|
||||
{
|
||||
let mut items = Vec::new();
|
||||
while let Some(row) = rows.try_next().await.map_postgres_err()? {
|
||||
items.push(mapper(&row)?);
|
||||
}
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
fn map_key_stats_row(row: &PgRow) -> Result<StoredProviderCatalogKeyStats, DataLayerError> {
|
||||
StoredProviderCatalogKeyStats::new(
|
||||
row_get(row, "provider_id")?,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use async_trait::async_trait;
|
||||
use futures_util::TryStreamExt;
|
||||
use sha2::{Digest, Sha256};
|
||||
use sqlx::{postgres::PgRow, PgPool, Row};
|
||||
|
||||
@@ -414,11 +415,12 @@ impl SqlxProxyNodeRepository {
|
||||
#[async_trait]
|
||||
impl ProxyNodeReadRepository for SqlxProxyNodeRepository {
|
||||
async fn list_proxy_nodes(&self) -> Result<Vec<StoredProxyNode>, DataLayerError> {
|
||||
let rows = sqlx::query(LIST_PROXY_NODES_SQL)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
rows.iter().map(Self::row_to_stored).collect()
|
||||
let mut rows = sqlx::query(LIST_PROXY_NODES_SQL).fetch(&self.pool);
|
||||
let mut items = Vec::new();
|
||||
while let Some(row) = rows.try_next().await.map_postgres_err()? {
|
||||
items.push(Self::row_to_stored(&row)?);
|
||||
}
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
async fn find_proxy_node(
|
||||
@@ -438,13 +440,15 @@ impl ProxyNodeReadRepository for SqlxProxyNodeRepository {
|
||||
node_id: &str,
|
||||
limit: usize,
|
||||
) -> Result<Vec<StoredProxyNodeEvent>, DataLayerError> {
|
||||
let rows = sqlx::query(LIST_PROXY_NODE_EVENTS_SQL)
|
||||
let mut rows = sqlx::query(LIST_PROXY_NODE_EVENTS_SQL)
|
||||
.bind(node_id)
|
||||
.bind(i64::try_from(limit).unwrap_or(i64::MAX))
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
rows.iter().map(Self::row_to_event).collect()
|
||||
.fetch(&self.pool);
|
||||
let mut items = Vec::new();
|
||||
while let Some(row) = rows.try_next().await.map_postgres_err()? {
|
||||
items.push(Self::row_to_event(&row)?);
|
||||
}
|
||||
Ok(items)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use async_trait::async_trait;
|
||||
use futures_util::future::BoxFuture;
|
||||
use futures_util::TryStreamExt;
|
||||
use sqlx::{PgPool, Row};
|
||||
|
||||
use super::types::{
|
||||
@@ -161,17 +162,18 @@ impl SqlxShadowResultRepository {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let rows = sqlx::query(LIST_RECENT_SQL)
|
||||
let mut rows = sqlx::query(LIST_RECENT_SQL)
|
||||
.bind(i64::try_from(limit).map_err(|_| {
|
||||
DataLayerError::UnexpectedValue(format!(
|
||||
"invalid recent shadow result limit: {limit}"
|
||||
))
|
||||
})?)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
|
||||
rows.iter().map(map_shadow_result_row).collect()
|
||||
.fetch(&self.pool);
|
||||
let mut items = Vec::new();
|
||||
while let Some(row) = rows.try_next().await.map_postgres_err()? {
|
||||
items.push(map_shadow_result_row(&row)?);
|
||||
}
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
pub async fn upsert(
|
||||
|
||||
@@ -4,6 +4,7 @@ use aether_data_contracts::repository::usage::{
|
||||
use async_trait::async_trait;
|
||||
use flate2::{read::GzDecoder, write::GzEncoder, Compression};
|
||||
use futures_util::future::BoxFuture;
|
||||
use futures_util::TryStreamExt;
|
||||
use serde_json::Map;
|
||||
use serde_json::Value;
|
||||
use sqlx::{PgPool, Postgres, QueryBuilder, Row};
|
||||
@@ -1181,12 +1182,13 @@ impl SqlxUsageReadRepository {
|
||||
}
|
||||
|
||||
builder.push(" ORDER BY \"usage\".created_at ASC, \"usage\".request_id ASC");
|
||||
let rows = builder
|
||||
.build()
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
rows.iter().map(|row| map_usage_row(row, false)).collect()
|
||||
let query = builder.build();
|
||||
let mut rows = query.fetch(&self.pool);
|
||||
let mut items = Vec::new();
|
||||
while let Some(row) = rows.try_next().await.map_postgres_err()? {
|
||||
items.push(map_usage_row(&row, false)?);
|
||||
}
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
pub async fn list_recent_usage_audits(
|
||||
@@ -1205,12 +1207,13 @@ impl SqlxUsageReadRepository {
|
||||
.push_bind(i64::try_from(limit).map_err(|_| {
|
||||
DataLayerError::InvalidInput(format!("invalid recent usage limit: {limit}"))
|
||||
})?);
|
||||
let rows = builder
|
||||
.build()
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
rows.iter().map(|row| map_usage_row(row, false)).collect()
|
||||
let query = builder.build();
|
||||
let mut rows = query.fetch(&self.pool);
|
||||
let mut items = Vec::new();
|
||||
while let Some(row) = rows.try_next().await.map_postgres_err()? {
|
||||
items.push(map_usage_row(&row, false)?);
|
||||
}
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
pub async fn summarize_total_tokens_by_api_key_ids(
|
||||
@@ -1221,14 +1224,12 @@ impl SqlxUsageReadRepository {
|
||||
return Ok(std::collections::BTreeMap::new());
|
||||
}
|
||||
|
||||
let rows = sqlx::query(SUMMARIZE_TOTAL_TOKENS_BY_API_KEY_IDS_SQL)
|
||||
let mut rows = sqlx::query(SUMMARIZE_TOTAL_TOKENS_BY_API_KEY_IDS_SQL)
|
||||
.bind(api_key_ids)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
.fetch(&self.pool);
|
||||
|
||||
let mut totals = std::collections::BTreeMap::new();
|
||||
for row in rows {
|
||||
while let Some(row) = rows.try_next().await.map_postgres_err()? {
|
||||
let api_key_id: String = row.try_get("api_key_id").map_postgres_err()?;
|
||||
let total_tokens = row
|
||||
.try_get::<i64, _>("total_tokens")
|
||||
@@ -1248,14 +1249,12 @@ impl SqlxUsageReadRepository {
|
||||
return Ok(std::collections::BTreeMap::new());
|
||||
}
|
||||
|
||||
let rows = sqlx::query(SUMMARIZE_USAGE_BY_PROVIDER_API_KEY_IDS_SQL)
|
||||
let mut rows = sqlx::query(SUMMARIZE_USAGE_BY_PROVIDER_API_KEY_IDS_SQL)
|
||||
.bind(provider_api_key_ids)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
.fetch(&self.pool);
|
||||
|
||||
let mut summaries = std::collections::BTreeMap::new();
|
||||
for row in rows {
|
||||
while let Some(row) = rows.try_next().await.map_postgres_err()? {
|
||||
let provider_api_key_id: String =
|
||||
row.try_get("provider_api_key_id").map_postgres_err()?;
|
||||
let request_count = row
|
||||
@@ -1988,7 +1987,7 @@ fn prepare_usage_body_storage(value: Option<&Value>) -> Result<UsageBodyStorage,
|
||||
let bytes = serde_json::to_vec(value).map_err(|err| {
|
||||
DataLayerError::UnexpectedValue(format!("failed to serialize usage json: {err}"))
|
||||
})?;
|
||||
if bytes.len() <= MAX_INLINE_USAGE_BODY_BYTES {
|
||||
if bytes.len() == MAX_INLINE_USAGE_BODY_BYTES {
|
||||
return Ok(UsageBodyStorage {
|
||||
inline_json: Some(String::from_utf8(bytes).map_err(|err| {
|
||||
DataLayerError::UnexpectedValue(format!(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use async_trait::async_trait;
|
||||
use futures_util::TryStreamExt;
|
||||
use sqlx::{PgPool, Postgres, QueryBuilder, Row};
|
||||
|
||||
use super::types::{
|
||||
@@ -189,30 +190,31 @@ impl SqlxUserReadRepository {
|
||||
if user_ids.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let rows = sqlx::query(LIST_USERS_BY_IDS_SQL)
|
||||
.bind(user_ids)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
rows.iter().map(map_user_row).collect()
|
||||
collect_query_rows(
|
||||
sqlx::query(LIST_USERS_BY_IDS_SQL)
|
||||
.bind(user_ids)
|
||||
.fetch(&self.pool),
|
||||
map_user_row,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn list_non_admin_export_users(
|
||||
&self,
|
||||
) -> Result<Vec<StoredUserExportRow>, DataLayerError> {
|
||||
let rows = sqlx::query(LIST_NON_ADMIN_EXPORT_USERS_SQL)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
rows.iter().map(map_user_export_row).collect()
|
||||
collect_query_rows(
|
||||
sqlx::query(LIST_NON_ADMIN_EXPORT_USERS_SQL).fetch(&self.pool),
|
||||
map_user_export_row,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn list_export_users(&self) -> Result<Vec<StoredUserExportRow>, DataLayerError> {
|
||||
let rows = sqlx::query(LIST_EXPORT_USERS_SQL)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
rows.iter().map(map_user_export_row).collect()
|
||||
collect_query_rows(
|
||||
sqlx::query(LIST_EXPORT_USERS_SQL).fetch(&self.pool),
|
||||
map_user_export_row,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn list_export_users_page(
|
||||
@@ -240,12 +242,8 @@ impl SqlxUserReadRepository {
|
||||
DataLayerError::InvalidInput(format!("invalid user export limit: {}", query.limit))
|
||||
})?);
|
||||
|
||||
let rows = builder
|
||||
.build()
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
rows.iter().map(map_user_export_row).collect()
|
||||
let query = builder.build();
|
||||
collect_query_rows(query.fetch(&self.pool), map_user_export_row).await
|
||||
}
|
||||
|
||||
pub async fn summarize_export_users(&self) -> Result<UserExportSummary, DataLayerError> {
|
||||
@@ -279,12 +277,13 @@ impl SqlxUserReadRepository {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let rows = sqlx::query(LIST_USER_AUTH_BY_IDS_SQL)
|
||||
.bind(user_ids)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
rows.iter().map(map_user_auth_row).collect()
|
||||
collect_query_rows(
|
||||
sqlx::query(LIST_USER_AUTH_BY_IDS_SQL)
|
||||
.bind(user_ids)
|
||||
.fetch(&self.pool),
|
||||
map_user_auth_row,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn find_user_auth_by_id(
|
||||
@@ -361,6 +360,20 @@ fn map_user_auth_row(row: &sqlx::postgres::PgRow) -> Result<StoredUserAuthRecord
|
||||
)
|
||||
}
|
||||
|
||||
async fn collect_query_rows<T, S>(
|
||||
mut rows: S,
|
||||
mapper: fn(&sqlx::postgres::PgRow) -> Result<T, DataLayerError>,
|
||||
) -> Result<Vec<T>, DataLayerError>
|
||||
where
|
||||
S: futures_util::TryStream<Ok = sqlx::postgres::PgRow, Error = sqlx::Error> + Unpin,
|
||||
{
|
||||
let mut items = Vec::new();
|
||||
while let Some(row) = rows.try_next().await.map_postgres_err()? {
|
||||
items.push(mapper(&row)?);
|
||||
}
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl UserReadRepository for SqlxUserReadRepository {
|
||||
async fn list_users_by_ids(
|
||||
|
||||
@@ -187,6 +187,15 @@ impl VideoTaskReadRepository for InMemoryVideoTaskRepository {
|
||||
Ok(tasks.into_iter().skip(offset).take(limit).collect())
|
||||
}
|
||||
|
||||
async fn list_page_summary(
|
||||
&self,
|
||||
filter: &VideoTaskQueryFilter,
|
||||
offset: usize,
|
||||
limit: usize,
|
||||
) -> Result<Vec<StoredVideoTask>, DataLayerError> {
|
||||
Self::list_page(self, filter, offset, limit).await
|
||||
}
|
||||
|
||||
async fn count(&self, filter: &VideoTaskQueryFilter) -> Result<u64, DataLayerError> {
|
||||
Ok(self
|
||||
.index
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use sqlx::{PgPool, Postgres, QueryBuilder, Row};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use futures_util::{stream::TryStream, TryStreamExt};
|
||||
use sqlx::{postgres::PgRow, PgPool, Postgres, QueryBuilder, Row};
|
||||
|
||||
use crate::error::SqlxResultExt;
|
||||
use crate::repository::video_tasks::{
|
||||
@@ -10,7 +10,7 @@ use crate::repository::video_tasks::{
|
||||
};
|
||||
use crate::DataLayerError;
|
||||
|
||||
const SELECT_VIDEO_TASK_COLUMNS: &str = r#"
|
||||
const SELECT_VIDEO_TASK_COLUMNS_PREFIX: &str = r#"
|
||||
id,
|
||||
short_id,
|
||||
request_id,
|
||||
@@ -26,12 +26,9 @@ const SELECT_VIDEO_TASK_COLUMNS: &str = r#"
|
||||
provider_api_format,
|
||||
format_converted,
|
||||
model,
|
||||
prompt,
|
||||
original_request_body,
|
||||
duration_seconds,
|
||||
resolution,
|
||||
aspect_ratio,
|
||||
size,
|
||||
"#;
|
||||
|
||||
const SELECT_VIDEO_TASK_COLUMNS_SUFFIX: &str = r#"
|
||||
status,
|
||||
progress_percent,
|
||||
progress_message,
|
||||
@@ -50,8 +47,53 @@ const SELECT_VIDEO_TASK_COLUMNS: &str = r#"
|
||||
request_metadata
|
||||
"#;
|
||||
|
||||
fn select_video_task_columns(
|
||||
prompt_sql: &str,
|
||||
original_request_body_sql: &str,
|
||||
duration_seconds_sql: &str,
|
||||
resolution_sql: &str,
|
||||
aspect_ratio_sql: &str,
|
||||
size_sql: &str,
|
||||
) -> String {
|
||||
format!(
|
||||
"{SELECT_VIDEO_TASK_COLUMNS_PREFIX}
|
||||
{prompt_sql} AS prompt,
|
||||
{original_request_body_sql} AS original_request_body,
|
||||
{duration_seconds_sql} AS duration_seconds,
|
||||
{resolution_sql} AS resolution,
|
||||
{aspect_ratio_sql} AS aspect_ratio,
|
||||
{size_sql} AS size,
|
||||
{SELECT_VIDEO_TASK_COLUMNS_SUFFIX}"
|
||||
)
|
||||
}
|
||||
|
||||
fn select_video_task_full_columns() -> String {
|
||||
select_video_task_columns(
|
||||
"prompt",
|
||||
"original_request_body",
|
||||
"duration_seconds",
|
||||
"resolution",
|
||||
"aspect_ratio",
|
||||
"size",
|
||||
)
|
||||
}
|
||||
|
||||
fn select_video_task_claim_columns() -> String {
|
||||
select_video_task_columns(
|
||||
"NULL::TEXT",
|
||||
"NULL::jsonb",
|
||||
"NULL::INTEGER",
|
||||
"NULL::TEXT",
|
||||
"NULL::TEXT",
|
||||
"NULL::TEXT",
|
||||
)
|
||||
}
|
||||
|
||||
fn select_video_task_sql(where_clause: &str) -> String {
|
||||
format!("SELECT\n{SELECT_VIDEO_TASK_COLUMNS}\nFROM video_tasks\n{where_clause}\n")
|
||||
format!(
|
||||
"SELECT\n{}\nFROM video_tasks\n{where_clause}\n",
|
||||
select_video_task_full_columns()
|
||||
)
|
||||
}
|
||||
|
||||
fn find_by_id_sql() -> String {
|
||||
@@ -76,7 +118,54 @@ fn list_due_sql() -> String {
|
||||
)
|
||||
}
|
||||
|
||||
fn select_video_task_page_summary_columns() -> &'static str {
|
||||
r#"
|
||||
id,
|
||||
NULL::TEXT AS short_id,
|
||||
request_id,
|
||||
user_id,
|
||||
NULL::TEXT AS api_key_id,
|
||||
username,
|
||||
NULL::TEXT AS api_key_name,
|
||||
external_task_id,
|
||||
provider_id,
|
||||
NULL::TEXT AS endpoint_id,
|
||||
NULL::TEXT AS key_id,
|
||||
NULL::TEXT AS client_api_format,
|
||||
NULL::TEXT AS provider_api_format,
|
||||
FALSE AS format_converted,
|
||||
model,
|
||||
CASE
|
||||
WHEN prompt IS NULL THEN NULL
|
||||
WHEN char_length(prompt) <= 100 THEN prompt
|
||||
ELSE LEFT(prompt, 100) || '...'
|
||||
END AS prompt,
|
||||
NULL::jsonb AS original_request_body,
|
||||
duration_seconds,
|
||||
resolution,
|
||||
aspect_ratio,
|
||||
NULL::TEXT AS size,
|
||||
status,
|
||||
progress_percent,
|
||||
progress_message,
|
||||
0::INTEGER AS retry_count,
|
||||
1::INTEGER AS poll_interval_seconds,
|
||||
NULL::BIGINT AS next_poll_at_unix_secs,
|
||||
poll_count,
|
||||
max_poll_count,
|
||||
CAST(EXTRACT(EPOCH FROM created_at) AS BIGINT) AS created_at_unix_ms,
|
||||
CAST(EXTRACT(EPOCH FROM submitted_at) AS BIGINT) AS submitted_at_unix_secs,
|
||||
CAST(EXTRACT(EPOCH FROM completed_at) AS BIGINT) AS completed_at_unix_secs,
|
||||
CAST(EXTRACT(EPOCH FROM updated_at) AS BIGINT) AS updated_at_unix_secs,
|
||||
error_code,
|
||||
error_message,
|
||||
video_url,
|
||||
NULL::jsonb AS request_metadata
|
||||
"#
|
||||
}
|
||||
|
||||
fn claim_due_sql() -> String {
|
||||
let columns = select_video_task_claim_columns();
|
||||
format!(
|
||||
"WITH due AS (
|
||||
SELECT id
|
||||
@@ -94,12 +183,13 @@ SET next_poll_at = TO_TIMESTAMP($4),
|
||||
updated_at = TO_TIMESTAMP($5)
|
||||
WHERE id IN (SELECT id FROM due)
|
||||
RETURNING
|
||||
{SELECT_VIDEO_TASK_COLUMNS}
|
||||
{columns}
|
||||
"
|
||||
)
|
||||
}
|
||||
|
||||
fn upsert_sql() -> String {
|
||||
let columns = select_video_task_full_columns();
|
||||
format!(
|
||||
"INSERT INTO video_tasks (
|
||||
id,
|
||||
@@ -216,12 +306,13 @@ ON CONFLICT (id) DO UPDATE SET
|
||||
completed_at = EXCLUDED.completed_at,
|
||||
updated_at = EXCLUDED.updated_at
|
||||
RETURNING
|
||||
{SELECT_VIDEO_TASK_COLUMNS}
|
||||
{columns}
|
||||
"
|
||||
)
|
||||
}
|
||||
|
||||
fn update_if_active_sql() -> String {
|
||||
let columns = select_video_task_full_columns();
|
||||
format!(
|
||||
"UPDATE video_tasks SET
|
||||
short_id = $2,
|
||||
@@ -263,7 +354,7 @@ fn update_if_active_sql() -> String {
|
||||
WHERE id = $1
|
||||
AND status = ANY($38)
|
||||
RETURNING
|
||||
{SELECT_VIDEO_TASK_COLUMNS}
|
||||
{columns}
|
||||
"
|
||||
)
|
||||
}
|
||||
@@ -343,16 +434,16 @@ impl SqlxVideoTaskRepository {
|
||||
|
||||
let active_statuses = vec!["pending", "submitted", "queued", "processing"];
|
||||
let sql = list_active_sql();
|
||||
let rows = sqlx::query(&sql)
|
||||
.bind(active_statuses)
|
||||
.bind(i64::try_from(limit).map_err(|_| {
|
||||
DataLayerError::UnexpectedValue(format!("invalid active task limit: {limit}"))
|
||||
})?)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
|
||||
rows.iter().map(map_video_task_row).collect()
|
||||
collect_query_rows(
|
||||
sqlx::query(&sql)
|
||||
.bind(active_statuses)
|
||||
.bind(i64::try_from(limit).map_err(|_| {
|
||||
DataLayerError::UnexpectedValue(format!("invalid active task limit: {limit}"))
|
||||
})?)
|
||||
.fetch(&self.pool),
|
||||
map_video_task_row,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn list_due(
|
||||
@@ -366,17 +457,17 @@ impl SqlxVideoTaskRepository {
|
||||
|
||||
let active_statuses = vec!["submitted", "queued", "processing"];
|
||||
let sql = list_due_sql();
|
||||
let rows = sqlx::query(&sql)
|
||||
.bind(active_statuses)
|
||||
.bind(now_unix_secs as f64)
|
||||
.bind(i64::try_from(limit).map_err(|_| {
|
||||
DataLayerError::UnexpectedValue(format!("invalid due task limit: {limit}"))
|
||||
})?)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
|
||||
rows.iter().map(map_video_task_row).collect()
|
||||
collect_query_rows(
|
||||
sqlx::query(&sql)
|
||||
.bind(active_statuses)
|
||||
.bind(now_unix_secs as f64)
|
||||
.bind(i64::try_from(limit).map_err(|_| {
|
||||
DataLayerError::UnexpectedValue(format!("invalid due task limit: {limit}"))
|
||||
})?)
|
||||
.fetch(&self.pool),
|
||||
map_video_task_row,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn list_page(
|
||||
@@ -395,7 +486,7 @@ impl SqlxVideoTaskRepository {
|
||||
.map_err(|_| DataLayerError::UnexpectedValue(format!("invalid limit: {limit}")))?;
|
||||
|
||||
let mut builder = QueryBuilder::<Postgres>::new("SELECT\n");
|
||||
builder.push(SELECT_VIDEO_TASK_COLUMNS);
|
||||
builder.push(select_video_task_full_columns());
|
||||
builder.push("\nFROM video_tasks");
|
||||
push_video_task_filter(&mut builder, filter, None);
|
||||
builder.push("\nORDER BY created_at DESC, updated_at DESC");
|
||||
@@ -404,12 +495,37 @@ impl SqlxVideoTaskRepository {
|
||||
builder.push("\nLIMIT ");
|
||||
builder.push_bind(limit);
|
||||
|
||||
let rows = builder
|
||||
.build()
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
rows.iter().map(map_video_task_row).collect()
|
||||
let query = builder.build();
|
||||
collect_query_rows(query.fetch(&self.pool), map_video_task_row).await
|
||||
}
|
||||
|
||||
pub async fn list_page_summary(
|
||||
&self,
|
||||
filter: &VideoTaskQueryFilter,
|
||||
offset: usize,
|
||||
limit: usize,
|
||||
) -> Result<Vec<StoredVideoTask>, DataLayerError> {
|
||||
if limit == 0 {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let offset = i64::try_from(offset)
|
||||
.map_err(|_| DataLayerError::UnexpectedValue(format!("invalid offset: {offset}")))?;
|
||||
let limit = i64::try_from(limit)
|
||||
.map_err(|_| DataLayerError::UnexpectedValue(format!("invalid limit: {limit}")))?;
|
||||
|
||||
let mut builder = QueryBuilder::<Postgres>::new("SELECT\n");
|
||||
builder.push(select_video_task_page_summary_columns());
|
||||
builder.push("\nFROM video_tasks");
|
||||
push_video_task_filter(&mut builder, filter, None);
|
||||
builder.push("\nORDER BY created_at DESC, updated_at DESC");
|
||||
builder.push("\nOFFSET ");
|
||||
builder.push_bind(offset);
|
||||
builder.push("\nLIMIT ");
|
||||
builder.push_bind(limit);
|
||||
|
||||
let query = builder.build();
|
||||
collect_query_rows(query.fetch(&self.pool), map_video_task_row).await
|
||||
}
|
||||
|
||||
pub async fn count(&self, filter: &VideoTaskQueryFilter) -> Result<u64, DataLayerError> {
|
||||
@@ -436,29 +552,29 @@ impl SqlxVideoTaskRepository {
|
||||
push_video_task_filter(&mut builder, filter, None);
|
||||
builder.push("\nGROUP BY status\nORDER BY status ASC");
|
||||
|
||||
let rows = builder
|
||||
.build()
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
rows.into_iter()
|
||||
.map(|row| {
|
||||
let query = builder.build();
|
||||
let mut rows = query.fetch(&self.pool);
|
||||
let mut items = Vec::new();
|
||||
while let Some(row) = rows.try_next().await.map_postgres_err()? {
|
||||
let entry = {
|
||||
let status = VideoTaskStatus::from_database(
|
||||
row.try_get::<String, _>("status")
|
||||
.map_postgres_err()?
|
||||
.as_str(),
|
||||
)?;
|
||||
let total = row.try_get::<i64, _>("total").map_postgres_err()?;
|
||||
Ok(VideoTaskStatusCount {
|
||||
VideoTaskStatusCount {
|
||||
status,
|
||||
count: u64::try_from(total).map_err(|_| {
|
||||
DataLayerError::UnexpectedValue(format!(
|
||||
"invalid status count result: {total}"
|
||||
))
|
||||
})?,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
};
|
||||
items.push(entry);
|
||||
}
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
pub async fn count_distinct_users(
|
||||
@@ -504,25 +620,25 @@ impl SqlxVideoTaskRepository {
|
||||
builder.push("\nGROUP BY model\nORDER BY total DESC, model ASC\nLIMIT ");
|
||||
builder.push_bind(limit);
|
||||
|
||||
let rows = builder
|
||||
.build()
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
rows.into_iter()
|
||||
.map(|row| {
|
||||
let query = builder.build();
|
||||
let mut rows = query.fetch(&self.pool);
|
||||
let mut items = Vec::new();
|
||||
while let Some(row) = rows.try_next().await.map_postgres_err()? {
|
||||
let entry = {
|
||||
let model = row.try_get::<String, _>("model").map_postgres_err()?;
|
||||
let total = row.try_get::<i64, _>("total").map_postgres_err()?;
|
||||
Ok(VideoTaskModelCount {
|
||||
VideoTaskModelCount {
|
||||
model,
|
||||
count: u64::try_from(total).map_err(|_| {
|
||||
DataLayerError::UnexpectedValue(format!(
|
||||
"invalid model count result: {total}"
|
||||
))
|
||||
})?,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
};
|
||||
items.push(entry);
|
||||
}
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
pub async fn count_created_since(
|
||||
@@ -693,20 +809,17 @@ impl SqlxVideoTaskRepository {
|
||||
let limit = i64::try_from(limit)
|
||||
.map_err(|_| DataLayerError::UnexpectedValue(format!("invalid limit: {limit}")))?;
|
||||
let sql = claim_due_sql();
|
||||
let rows = sqlx::query(&sql)
|
||||
.bind(vec!["submitted", "queued", "processing"])
|
||||
.bind(now_unix_secs as f64)
|
||||
.bind(limit)
|
||||
.bind(claim_until_unix_secs as f64)
|
||||
.bind(now_unix_secs as f64)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
|
||||
let mut tasks = rows
|
||||
.iter()
|
||||
.map(map_video_task_row)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let mut tasks = collect_query_rows(
|
||||
sqlx::query(&sql)
|
||||
.bind(vec!["submitted", "queued", "processing"])
|
||||
.bind(now_unix_secs as f64)
|
||||
.bind(limit)
|
||||
.bind(claim_until_unix_secs as f64)
|
||||
.bind(now_unix_secs as f64)
|
||||
.fetch(&self.pool),
|
||||
map_video_task_row,
|
||||
)
|
||||
.await?;
|
||||
tasks.sort_by(|left, right| {
|
||||
left.next_poll_at_unix_secs
|
||||
.cmp(&right.next_poll_at_unix_secs)
|
||||
@@ -746,6 +859,15 @@ impl VideoTaskReadRepository for SqlxVideoTaskRepository {
|
||||
Self::list_page(self, filter, offset, limit).await
|
||||
}
|
||||
|
||||
async fn list_page_summary(
|
||||
&self,
|
||||
filter: &VideoTaskQueryFilter,
|
||||
offset: usize,
|
||||
limit: usize,
|
||||
) -> Result<Vec<StoredVideoTask>, DataLayerError> {
|
||||
Self::list_page_summary(self, filter, offset, limit).await
|
||||
}
|
||||
|
||||
async fn count(&self, filter: &VideoTaskQueryFilter) -> Result<u64, DataLayerError> {
|
||||
Self::count(self, filter).await
|
||||
}
|
||||
@@ -881,7 +1003,21 @@ fn map_status_for_database(status: VideoTaskStatus) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
fn map_video_task_row(row: &sqlx::postgres::PgRow) -> Result<StoredVideoTask, DataLayerError> {
|
||||
async fn collect_query_rows<T, S>(
|
||||
mut rows: S,
|
||||
map_row: fn(&PgRow) -> Result<T, DataLayerError>,
|
||||
) -> Result<Vec<T>, DataLayerError>
|
||||
where
|
||||
S: TryStream<Ok = PgRow, Error = sqlx::Error> + Unpin,
|
||||
{
|
||||
let mut items = Vec::new();
|
||||
while let Some(row) = rows.try_next().await.map_postgres_err()? {
|
||||
items.push(map_row(&row)?);
|
||||
}
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
fn map_video_task_row(row: &PgRow) -> Result<StoredVideoTask, DataLayerError> {
|
||||
let status = VideoTaskStatus::from_database(
|
||||
row.try_get::<String, _>("status")
|
||||
.map_postgres_err()?
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use async_trait::async_trait;
|
||||
use chrono::Utc;
|
||||
use futures_util::{stream::TryStream, TryStreamExt};
|
||||
use sqlx::{postgres::PgRow, PgPool, Row};
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -24,7 +25,6 @@ use crate::{
|
||||
postgres::PostgresTransactionRunner,
|
||||
DataLayerError,
|
||||
};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
const FIND_BY_WALLET_ID_SQL: &str = r#"
|
||||
SELECT
|
||||
@@ -638,21 +638,13 @@ impl WalletReadRepository for SqlxWalletRepository {
|
||||
if user_ids.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let mut ids_map = BTreeMap::new();
|
||||
for (index, id) in user_ids.iter().enumerate() {
|
||||
ids_map.entry(id).or_insert_with(Vec::new).push(index);
|
||||
}
|
||||
let rows = sqlx::query(LIST_BY_USER_IDS_SQL)
|
||||
.bind(user_ids)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
let mut wallets = Vec::with_capacity(rows.len());
|
||||
for row in rows {
|
||||
let wallet = map_wallet_row(&row)?;
|
||||
wallets.push(wallet);
|
||||
}
|
||||
Ok(wallets)
|
||||
collect_query_rows(
|
||||
sqlx::query(LIST_BY_USER_IDS_SQL)
|
||||
.bind(user_ids)
|
||||
.fetch(&self.pool),
|
||||
map_wallet_row,
|
||||
)
|
||||
.await
|
||||
}
|
||||
async fn list_wallets_by_api_key_ids(
|
||||
&self,
|
||||
@@ -661,17 +653,13 @@ impl WalletReadRepository for SqlxWalletRepository {
|
||||
if api_key_ids.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let rows = sqlx::query(LIST_BY_API_KEY_IDS_SQL)
|
||||
.bind(api_key_ids)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
let mut wallets = Vec::with_capacity(rows.len());
|
||||
for row in rows {
|
||||
let wallet = map_wallet_row(&row)?;
|
||||
wallets.push(wallet);
|
||||
}
|
||||
Ok(wallets)
|
||||
collect_query_rows(
|
||||
sqlx::query(LIST_BY_API_KEY_IDS_SQL)
|
||||
.bind(api_key_ids)
|
||||
.fetch(&self.pool),
|
||||
map_wallet_row,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn list_admin_wallets(
|
||||
@@ -686,18 +674,16 @@ impl WalletReadRepository for SqlxWalletRepository {
|
||||
.await
|
||||
.map_postgres_err()?,
|
||||
)?;
|
||||
let rows = sqlx::query(LIST_ADMIN_WALLETS_SQL)
|
||||
.bind(query.status.as_deref())
|
||||
.bind(query.owner_type.as_deref())
|
||||
.bind(as_i64(query.offset, "wallet offset")?)
|
||||
.bind(as_i64(query.limit, "wallet limit")?)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
let items = rows
|
||||
.iter()
|
||||
.map(map_admin_wallet_list_item_row)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let items = collect_query_rows(
|
||||
sqlx::query(LIST_ADMIN_WALLETS_SQL)
|
||||
.bind(query.status.as_deref())
|
||||
.bind(query.owner_type.as_deref())
|
||||
.bind(as_i64(query.offset, "wallet offset")?)
|
||||
.bind(as_i64(query.limit, "wallet limit")?)
|
||||
.fetch(&self.pool),
|
||||
map_admin_wallet_list_item_row,
|
||||
)
|
||||
.await?;
|
||||
Ok(StoredAdminWalletListPage { items, total })
|
||||
}
|
||||
|
||||
@@ -714,19 +700,17 @@ impl WalletReadRepository for SqlxWalletRepository {
|
||||
.await
|
||||
.map_postgres_err()?,
|
||||
)?;
|
||||
let rows = sqlx::query(LIST_ADMIN_WALLET_LEDGER_SQL)
|
||||
.bind(query.category.as_deref())
|
||||
.bind(query.reason_code.as_deref())
|
||||
.bind(query.owner_type.as_deref())
|
||||
.bind(as_i64(query.offset, "wallet ledger offset")?)
|
||||
.bind(as_i64(query.limit, "wallet ledger limit")?)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
let items = rows
|
||||
.iter()
|
||||
.map(map_admin_wallet_ledger_item_row)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let items = collect_query_rows(
|
||||
sqlx::query(LIST_ADMIN_WALLET_LEDGER_SQL)
|
||||
.bind(query.category.as_deref())
|
||||
.bind(query.reason_code.as_deref())
|
||||
.bind(query.owner_type.as_deref())
|
||||
.bind(as_i64(query.offset, "wallet ledger offset")?)
|
||||
.bind(as_i64(query.limit, "wallet ledger limit")?)
|
||||
.fetch(&self.pool),
|
||||
map_admin_wallet_ledger_item_row,
|
||||
)
|
||||
.await?;
|
||||
Ok(StoredAdminWalletLedgerPage { items, total })
|
||||
}
|
||||
|
||||
@@ -741,17 +725,15 @@ impl WalletReadRepository for SqlxWalletRepository {
|
||||
.await
|
||||
.map_postgres_err()?,
|
||||
)?;
|
||||
let rows = sqlx::query(LIST_ADMIN_WALLET_REFUND_REQUESTS_SQL)
|
||||
.bind(query.status.as_deref())
|
||||
.bind(as_i64(query.offset, "wallet refund request offset")?)
|
||||
.bind(as_i64(query.limit, "wallet refund request limit")?)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
let items = rows
|
||||
.iter()
|
||||
.map(map_admin_wallet_refund_request_item_row)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let items = collect_query_rows(
|
||||
sqlx::query(LIST_ADMIN_WALLET_REFUND_REQUESTS_SQL)
|
||||
.bind(query.status.as_deref())
|
||||
.bind(as_i64(query.offset, "wallet refund request offset")?)
|
||||
.bind(as_i64(query.limit, "wallet refund request limit")?)
|
||||
.fetch(&self.pool),
|
||||
map_admin_wallet_refund_request_item_row,
|
||||
)
|
||||
.await?;
|
||||
Ok(StoredAdminWalletRefundRequestPage { items, total })
|
||||
}
|
||||
|
||||
@@ -768,17 +750,15 @@ impl WalletReadRepository for SqlxWalletRepository {
|
||||
.await
|
||||
.map_postgres_err()?,
|
||||
)?;
|
||||
let rows = sqlx::query(LIST_ADMIN_WALLET_TRANSACTIONS_SQL)
|
||||
.bind(wallet_id)
|
||||
.bind(as_i64(offset, "wallet transaction offset")?)
|
||||
.bind(as_i64(limit, "wallet transaction limit")?)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
let items = rows
|
||||
.iter()
|
||||
.map(map_admin_wallet_transaction_row)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let items = collect_query_rows(
|
||||
sqlx::query(LIST_ADMIN_WALLET_TRANSACTIONS_SQL)
|
||||
.bind(wallet_id)
|
||||
.bind(as_i64(offset, "wallet transaction offset")?)
|
||||
.bind(as_i64(limit, "wallet transaction limit")?)
|
||||
.fetch(&self.pool),
|
||||
map_admin_wallet_transaction_row,
|
||||
)
|
||||
.await?;
|
||||
Ok(StoredAdminWalletTransactionPage { items, total })
|
||||
}
|
||||
|
||||
@@ -810,17 +790,15 @@ impl WalletReadRepository for SqlxWalletRepository {
|
||||
.await
|
||||
.map_postgres_err()?,
|
||||
)?;
|
||||
let rows = sqlx::query(LIST_WALLET_DAILY_USAGE_HISTORY_SQL)
|
||||
.bind(wallet_id)
|
||||
.bind(billing_timezone)
|
||||
.bind(as_i64(limit, "wallet daily usage history limit")?)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
let items = rows
|
||||
.iter()
|
||||
.map(map_wallet_daily_usage_row)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let items = collect_query_rows(
|
||||
sqlx::query(LIST_WALLET_DAILY_USAGE_HISTORY_SQL)
|
||||
.bind(wallet_id)
|
||||
.bind(billing_timezone)
|
||||
.bind(as_i64(limit, "wallet daily usage history limit")?)
|
||||
.fetch(&self.pool),
|
||||
map_wallet_daily_usage_row,
|
||||
)
|
||||
.await?;
|
||||
Ok(StoredWalletDailyUsageLedgerPage { items, total })
|
||||
}
|
||||
|
||||
@@ -837,17 +815,15 @@ impl WalletReadRepository for SqlxWalletRepository {
|
||||
.await
|
||||
.map_postgres_err()?,
|
||||
)?;
|
||||
let rows = sqlx::query(LIST_ADMIN_WALLET_REFUNDS_SQL)
|
||||
.bind(wallet_id)
|
||||
.bind(as_i64(offset, "wallet refund offset")?)
|
||||
.bind(as_i64(limit, "wallet refund limit")?)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
let items = rows
|
||||
.iter()
|
||||
.map(map_admin_wallet_refund_row)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let items = collect_query_rows(
|
||||
sqlx::query(LIST_ADMIN_WALLET_REFUNDS_SQL)
|
||||
.bind(wallet_id)
|
||||
.bind(as_i64(offset, "wallet refund offset")?)
|
||||
.bind(as_i64(limit, "wallet refund limit")?)
|
||||
.fetch(&self.pool),
|
||||
map_admin_wallet_refund_row,
|
||||
)
|
||||
.await?;
|
||||
Ok(StoredAdminWalletRefundPage { items, total })
|
||||
}
|
||||
|
||||
@@ -863,18 +839,16 @@ impl WalletReadRepository for SqlxWalletRepository {
|
||||
.await
|
||||
.map_postgres_err()?,
|
||||
)?;
|
||||
let rows = sqlx::query(LIST_ADMIN_PAYMENT_ORDERS_SQL)
|
||||
.bind(query.payment_method.as_deref())
|
||||
.bind(query.status.as_deref())
|
||||
.bind(as_i64(query.offset, "payment order offset")?)
|
||||
.bind(as_i64(query.limit, "payment order limit")?)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
let items = rows
|
||||
.iter()
|
||||
.map(map_admin_payment_order_row)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let items = collect_query_rows(
|
||||
sqlx::query(LIST_ADMIN_PAYMENT_ORDERS_SQL)
|
||||
.bind(query.payment_method.as_deref())
|
||||
.bind(query.status.as_deref())
|
||||
.bind(as_i64(query.offset, "payment order offset")?)
|
||||
.bind(as_i64(query.limit, "payment order limit")?)
|
||||
.fetch(&self.pool),
|
||||
map_admin_payment_order_row,
|
||||
)
|
||||
.await?;
|
||||
Ok(StoredAdminPaymentOrderPage { items, total })
|
||||
}
|
||||
|
||||
@@ -903,17 +877,15 @@ impl WalletReadRepository for SqlxWalletRepository {
|
||||
.await
|
||||
.map_postgres_err()?,
|
||||
)?;
|
||||
let rows = sqlx::query(LIST_WALLET_PAYMENT_ORDERS_BY_USER_SQL)
|
||||
.bind(user_id)
|
||||
.bind(as_i64(offset, "wallet payment order offset")?)
|
||||
.bind(as_i64(limit, "wallet payment order limit")?)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
let items = rows
|
||||
.iter()
|
||||
.map(map_admin_payment_order_row)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let items = collect_query_rows(
|
||||
sqlx::query(LIST_WALLET_PAYMENT_ORDERS_BY_USER_SQL)
|
||||
.bind(user_id)
|
||||
.bind(as_i64(offset, "wallet payment order offset")?)
|
||||
.bind(as_i64(limit, "wallet payment order limit")?)
|
||||
.fetch(&self.pool),
|
||||
map_admin_payment_order_row,
|
||||
)
|
||||
.await?;
|
||||
Ok(StoredAdminPaymentOrderPage { items, total })
|
||||
}
|
||||
|
||||
@@ -958,17 +930,15 @@ impl WalletReadRepository for SqlxWalletRepository {
|
||||
.await
|
||||
.map_postgres_err()?,
|
||||
)?;
|
||||
let rows = sqlx::query(LIST_ADMIN_PAYMENT_CALLBACKS_SQL)
|
||||
.bind(payment_method)
|
||||
.bind(as_i64(offset, "payment callback offset")?)
|
||||
.bind(as_i64(limit, "payment callback limit")?)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
let items = rows
|
||||
.iter()
|
||||
.map(map_admin_payment_callback_row)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let items = collect_query_rows(
|
||||
sqlx::query(LIST_ADMIN_PAYMENT_CALLBACKS_SQL)
|
||||
.bind(payment_method)
|
||||
.bind(as_i64(offset, "payment callback offset")?)
|
||||
.bind(as_i64(limit, "payment callback limit")?)
|
||||
.fetch(&self.pool),
|
||||
map_admin_payment_callback_row,
|
||||
)
|
||||
.await?;
|
||||
Ok(StoredAdminPaymentCallbackPage { items, total })
|
||||
}
|
||||
}
|
||||
@@ -3556,6 +3526,20 @@ fn read_count(row: PgRow) -> Result<u64, DataLayerError> {
|
||||
Ok(total.max(0) as u64)
|
||||
}
|
||||
|
||||
async fn collect_query_rows<T, S>(
|
||||
mut rows: S,
|
||||
map_row: fn(&PgRow) -> Result<T, DataLayerError>,
|
||||
) -> Result<Vec<T>, DataLayerError>
|
||||
where
|
||||
S: TryStream<Ok = PgRow, Error = sqlx::Error> + Unpin,
|
||||
{
|
||||
let mut items = Vec::new();
|
||||
while let Some(row) = rows.try_next().await.map_postgres_err()? {
|
||||
items.push(map_row(&row)?);
|
||||
}
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
fn map_admin_wallet_list_item_row(
|
||||
row: &PgRow,
|
||||
) -> Result<StoredAdminWalletListItem, DataLayerError> {
|
||||
|
||||
Reference in New Issue
Block a user