mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +08:00
refactor(data): limit query abstraction to postgres and sqlite
This commit is contained in:
@@ -4,14 +4,12 @@ use sqlx::{Database, Encode, QueryBuilder, Type};
|
||||
pub enum SqlDialect {
|
||||
Postgres,
|
||||
Sqlite,
|
||||
Mysql,
|
||||
}
|
||||
|
||||
impl SqlDialect {
|
||||
pub fn quote_ident(self, ident: &str) -> String {
|
||||
let quote = match self {
|
||||
Self::Postgres | Self::Sqlite => '"',
|
||||
Self::Mysql => '`',
|
||||
};
|
||||
let escaped = ident.replace(quote, &format!("{quote}{quote}"));
|
||||
format!("{quote}{escaped}{quote}")
|
||||
@@ -31,7 +29,6 @@ pub struct DialectSql<'a> {
|
||||
common: Option<&'a str>,
|
||||
postgres: Option<&'a str>,
|
||||
sqlite: Option<&'a str>,
|
||||
mysql: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl<'a> DialectSql<'a> {
|
||||
@@ -40,16 +37,14 @@ impl<'a> DialectSql<'a> {
|
||||
common: Some(sql),
|
||||
postgres: None,
|
||||
sqlite: None,
|
||||
mysql: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn dialect(postgres: &'a str, sqlite: &'a str, mysql: &'a str) -> Self {
|
||||
pub const fn dialect(postgres: &'a str, sqlite: &'a str) -> Self {
|
||||
Self {
|
||||
common: None,
|
||||
postgres: Some(postgres),
|
||||
sqlite: Some(sqlite),
|
||||
mysql: Some(mysql),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,16 +58,10 @@ impl<'a> DialectSql<'a> {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_mysql(mut self, sql: &'a str) -> Self {
|
||||
self.mysql = Some(sql);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn sql(self, dialect: SqlDialect) -> &'a str {
|
||||
match dialect {
|
||||
SqlDialect::Postgres => self.postgres.or(self.common),
|
||||
SqlDialect::Sqlite => self.sqlite.or(self.common),
|
||||
SqlDialect::Mysql => self.mysql.or(self.common),
|
||||
}
|
||||
.expect("dialect SQL expression is missing for selected dialect")
|
||||
}
|
||||
@@ -470,7 +459,7 @@ fn push_ci_contains_predicate<'args, DB>(
|
||||
.push(" ILIKE ")
|
||||
.push_bind(format!("%{trimmed}%"));
|
||||
}
|
||||
SqlDialect::Sqlite | SqlDialect::Mysql => {
|
||||
SqlDialect::Sqlite => {
|
||||
builder
|
||||
.push("LOWER(")
|
||||
.push(column_sql)
|
||||
@@ -522,13 +511,12 @@ pub fn push_order_by<DB>(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use sqlx::{Execute, MySql, Postgres, QueryBuilder, Sqlite};
|
||||
use sqlx::{Execute, Postgres, QueryBuilder, Sqlite};
|
||||
|
||||
#[test]
|
||||
fn quotes_identifiers_by_dialect() {
|
||||
assert_eq!(SqlDialect::Postgres.quote_ident("trigger"), "\"trigger\"");
|
||||
assert_eq!(SqlDialect::Sqlite.quote_ident("trigger"), "\"trigger\"");
|
||||
assert_eq!(SqlDialect::Mysql.quote_ident("trigger"), "`trigger`");
|
||||
assert_eq!(
|
||||
SqlDialect::Postgres.quote_path(&["usage", "id"]),
|
||||
"\"usage\".\"id\""
|
||||
@@ -571,7 +559,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ci_contains_uses_lower_like_for_sqlite_and_mysql() {
|
||||
fn ci_contains_uses_lower_like_for_sqlite() {
|
||||
let mut sqlite_builder = QueryBuilder::<Sqlite>::new("SELECT * FROM items");
|
||||
let mut sqlite_where = WhereClause::new();
|
||||
push_ci_contains(
|
||||
@@ -585,20 +573,6 @@ mod tests {
|
||||
.build()
|
||||
.sql()
|
||||
.contains(" WHERE LOWER(task_key) LIKE ?"));
|
||||
|
||||
let mut mysql_builder = QueryBuilder::<MySql>::new("SELECT * FROM items");
|
||||
let mut mysql_where = WhereClause::new();
|
||||
push_ci_contains(
|
||||
&mut mysql_builder,
|
||||
&mut mysql_where,
|
||||
SqlDialect::Mysql,
|
||||
"task_key",
|
||||
" Fetch ",
|
||||
);
|
||||
assert!(mysql_builder
|
||||
.build()
|
||||
.sql()
|
||||
.contains(" WHERE LOWER(task_key) LIKE ?"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -652,7 +626,6 @@ mod tests {
|
||||
SelectColumn::expr(DialectSql::dialect(
|
||||
"CAST(monthly_quota_usd AS DOUBLE PRECISION)",
|
||||
"CAST(monthly_quota_usd AS REAL)",
|
||||
"monthly_quota_usd",
|
||||
))
|
||||
.alias("monthly_quota_usd"),
|
||||
]);
|
||||
@@ -662,8 +635,8 @@ mod tests {
|
||||
"SELECT id AS \"provider_id\", CAST(monthly_quota_usd AS DOUBLE PRECISION) AS \"monthly_quota_usd\" FROM providers"
|
||||
);
|
||||
assert_eq!(
|
||||
query.render(SqlDialect::Mysql),
|
||||
"SELECT id AS `provider_id`, monthly_quota_usd AS `monthly_quota_usd` FROM providers"
|
||||
query.render(SqlDialect::Sqlite),
|
||||
"SELECT id AS \"provider_id\", CAST(monthly_quota_usd AS REAL) AS \"monthly_quota_usd\" FROM providers"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use async_trait::async_trait;
|
||||
use sqlx::{mysql::MySqlRow, MySql, QueryBuilder, Row};
|
||||
use sqlx::{mysql::MySqlRow, Row};
|
||||
|
||||
use super::types::{
|
||||
AnnouncementListQuery, AnnouncementReadRepository, AnnouncementWriteRepository,
|
||||
@@ -8,7 +8,6 @@ use super::types::{
|
||||
use crate::driver::mysql::MysqlPool;
|
||||
use crate::error::SqlResultExt;
|
||||
use crate::DataLayerError;
|
||||
use aether_data_query::{push_eq, push_limit, push_limit_offset, WhereClause};
|
||||
|
||||
const ANNOUNCEMENT_SELECT: &str = r#"
|
||||
SELECT
|
||||
@@ -45,27 +44,6 @@ impl MysqlAnnouncementRepository {
|
||||
) -> Result<Option<StoredAnnouncement>, DataLayerError> {
|
||||
self.find_by_id(announcement_id).await
|
||||
}
|
||||
|
||||
fn apply_active_filter(
|
||||
builder: &mut QueryBuilder<'_, MySql>,
|
||||
where_clause: &mut WhereClause,
|
||||
active_only: bool,
|
||||
now_unix_secs: u64,
|
||||
) -> Result<(), DataLayerError> {
|
||||
if !active_only {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let now = i64_from_u64(now_unix_secs, "announcements.now")?;
|
||||
where_clause.push_next(builder);
|
||||
builder
|
||||
.push("a.is_active = 1 AND (a.start_time IS NULL OR a.start_time <= ")
|
||||
.push_bind(now)
|
||||
.push(") AND (a.end_time IS NULL OR a.end_time >= ")
|
||||
.push_bind(now)
|
||||
.push(")");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -74,17 +52,8 @@ impl AnnouncementReadRepository for MysqlAnnouncementRepository {
|
||||
&self,
|
||||
announcement_id: &str,
|
||||
) -> Result<Option<StoredAnnouncement>, DataLayerError> {
|
||||
let mut builder = QueryBuilder::<MySql>::new(ANNOUNCEMENT_SELECT);
|
||||
let mut where_clause = WhereClause::new();
|
||||
push_eq(
|
||||
&mut builder,
|
||||
&mut where_clause,
|
||||
"a.id",
|
||||
announcement_id.to_string(),
|
||||
);
|
||||
push_limit(&mut builder, 1);
|
||||
let row = builder
|
||||
.build()
|
||||
let row = sqlx::query(&format!("{ANNOUNCEMENT_SELECT} WHERE a.id = ? LIMIT 1"))
|
||||
.bind(announcement_id)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
@@ -96,38 +65,49 @@ impl AnnouncementReadRepository for MysqlAnnouncementRepository {
|
||||
query: &AnnouncementListQuery,
|
||||
) -> Result<StoredAnnouncementPage, DataLayerError> {
|
||||
let now_unix_secs = query.now_unix_secs.unwrap_or_else(current_unix_secs);
|
||||
let mut count_builder =
|
||||
QueryBuilder::<MySql>::new("SELECT COUNT(a.id) AS total FROM announcements a");
|
||||
let mut count_where = WhereClause::new();
|
||||
Self::apply_active_filter(
|
||||
&mut count_builder,
|
||||
&mut count_where,
|
||||
query.active_only,
|
||||
now_unix_secs,
|
||||
)?;
|
||||
let total = count_builder
|
||||
.build_query_scalar::<i64>()
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?
|
||||
.max(0) as u64;
|
||||
let total_row = sqlx::query(
|
||||
r#"
|
||||
SELECT COUNT(a.id) AS total
|
||||
FROM announcements a
|
||||
WHERE (
|
||||
NOT ? OR (
|
||||
a.is_active = 1
|
||||
AND (a.start_time IS NULL OR a.start_time <= ?)
|
||||
AND (a.end_time IS NULL OR a.end_time >= ?)
|
||||
)
|
||||
)
|
||||
"#,
|
||||
)
|
||||
.bind(query.active_only)
|
||||
.bind(now_unix_secs as i64)
|
||||
.bind(now_unix_secs as i64)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
let total = total_row.try_get::<i64, _>("total").map_sql_err()?.max(0) as u64;
|
||||
|
||||
let mut list_builder = QueryBuilder::<MySql>::new(ANNOUNCEMENT_SELECT);
|
||||
let mut list_where = WhereClause::new();
|
||||
Self::apply_active_filter(
|
||||
&mut list_builder,
|
||||
&mut list_where,
|
||||
query.active_only,
|
||||
now_unix_secs,
|
||||
)?;
|
||||
list_builder
|
||||
.push(" ORDER BY a.is_pinned DESC, a.priority DESC, a.created_at DESC, a.id ASC");
|
||||
push_limit_offset(&mut list_builder, query.limit as i64, query.offset as i64);
|
||||
let rows = list_builder
|
||||
.build()
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
let rows = sqlx::query(&format!(
|
||||
r#"
|
||||
{ANNOUNCEMENT_SELECT}
|
||||
WHERE (
|
||||
NOT ? OR (
|
||||
a.is_active = 1
|
||||
AND (a.start_time IS NULL OR a.start_time <= ?)
|
||||
AND (a.end_time IS NULL OR a.end_time >= ?)
|
||||
)
|
||||
)
|
||||
ORDER BY a.is_pinned DESC, a.priority DESC, a.created_at DESC, a.id ASC
|
||||
LIMIT ? OFFSET ?
|
||||
"#
|
||||
))
|
||||
.bind(query.active_only)
|
||||
.bind(now_unix_secs as i64)
|
||||
.bind(now_unix_secs as i64)
|
||||
.bind(query.limit as i64)
|
||||
.bind(query.offset as i64)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
let items = rows
|
||||
.iter()
|
||||
.map(map_announcement_row)
|
||||
@@ -141,22 +121,28 @@ impl AnnouncementReadRepository for MysqlAnnouncementRepository {
|
||||
user_id: &str,
|
||||
now_unix_secs: u64,
|
||||
) -> Result<u64, DataLayerError> {
|
||||
let mut builder =
|
||||
QueryBuilder::<MySql>::new("SELECT COUNT(a.id) AS total FROM announcements a");
|
||||
let mut where_clause = WhereClause::new();
|
||||
Self::apply_active_filter(&mut builder, &mut where_clause, true, now_unix_secs)?;
|
||||
where_clause.push_next(&mut builder);
|
||||
builder
|
||||
.push("NOT EXISTS (SELECT 1 FROM announcement_reads r WHERE r.user_id = ")
|
||||
.push_bind(user_id.to_string())
|
||||
.push(" AND r.announcement_id = a.id)");
|
||||
let total = builder
|
||||
.build_query_scalar::<i64>()
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?
|
||||
.max(0) as u64;
|
||||
Ok(total)
|
||||
let row = sqlx::query(
|
||||
r#"
|
||||
SELECT COUNT(a.id) AS total
|
||||
FROM announcements a
|
||||
WHERE a.is_active = 1
|
||||
AND (a.start_time IS NULL OR a.start_time <= ?)
|
||||
AND (a.end_time IS NULL OR a.end_time >= ?)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM announcement_reads r
|
||||
WHERE r.user_id = ?
|
||||
AND r.announcement_id = a.id
|
||||
)
|
||||
"#,
|
||||
)
|
||||
.bind(now_unix_secs as i64)
|
||||
.bind(now_unix_secs as i64)
|
||||
.bind(user_id)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
Ok(row.try_get::<i64, _>("total").map_sql_err()?.max(0) as u64)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use async_trait::async_trait;
|
||||
use sqlx::{mysql::MySqlRow, MySql, QueryBuilder, Row};
|
||||
use sqlx::{mysql::MySqlRow, Row};
|
||||
|
||||
use super::types::{
|
||||
AuthModuleReadRepository, AuthModuleWriteRepository, StoredLdapModuleConfig,
|
||||
@@ -8,9 +8,8 @@ use super::types::{
|
||||
use crate::driver::mysql::MysqlPool;
|
||||
use crate::error::SqlResultExt;
|
||||
use crate::DataLayerError;
|
||||
use aether_data_query::{push_eq, push_limit, WhereClause};
|
||||
|
||||
const OAUTH_PROVIDER_COLUMNS: &str = r#"
|
||||
const LIST_ENABLED_OAUTH_PROVIDERS_SQL: &str = r#"
|
||||
SELECT
|
||||
provider_type,
|
||||
display_name,
|
||||
@@ -18,9 +17,11 @@ SELECT
|
||||
client_secret_encrypted,
|
||||
redirect_uri
|
||||
FROM oauth_providers
|
||||
WHERE is_enabled = 1
|
||||
ORDER BY provider_type ASC
|
||||
"#;
|
||||
|
||||
const LDAP_CONFIG_COLUMNS: &str = r#"
|
||||
const GET_LDAP_CONFIG_SQL: &str = r#"
|
||||
SELECT
|
||||
server_url,
|
||||
bind_dn,
|
||||
@@ -35,6 +36,8 @@ SELECT
|
||||
use_starttls,
|
||||
connect_timeout
|
||||
FROM ldap_configs
|
||||
ORDER BY id ASC
|
||||
LIMIT 1
|
||||
"#;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -59,37 +62,24 @@ impl MysqlAuthModuleRepository {
|
||||
}
|
||||
}
|
||||
|
||||
async fn list_enabled_oauth_providers(
|
||||
pool: &MysqlPool,
|
||||
) -> Result<Vec<StoredOAuthProviderModuleConfig>, DataLayerError> {
|
||||
let mut builder = QueryBuilder::<MySql>::new(OAUTH_PROVIDER_COLUMNS);
|
||||
let mut where_clause = WhereClause::new();
|
||||
push_eq(&mut builder, &mut where_clause, "is_enabled", true);
|
||||
builder.push(" ORDER BY provider_type ASC");
|
||||
let rows = builder.build().fetch_all(pool).await.map_sql_err()?;
|
||||
rows.iter().map(map_oauth_row).collect()
|
||||
}
|
||||
|
||||
async fn get_ldap_config(
|
||||
pool: &MysqlPool,
|
||||
) -> Result<Option<StoredLdapModuleConfig>, DataLayerError> {
|
||||
let mut builder = QueryBuilder::<MySql>::new(LDAP_CONFIG_COLUMNS);
|
||||
builder.push(" ORDER BY id ASC");
|
||||
push_limit(&mut builder, 1);
|
||||
let row = builder.build().fetch_optional(pool).await.map_sql_err()?;
|
||||
row.as_ref().map(map_ldap_row).transpose()
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl AuthModuleReadRepository for MysqlAuthModuleReadRepository {
|
||||
async fn list_enabled_oauth_providers(
|
||||
&self,
|
||||
) -> Result<Vec<StoredOAuthProviderModuleConfig>, DataLayerError> {
|
||||
list_enabled_oauth_providers(&self.pool).await
|
||||
let rows = sqlx::query(LIST_ENABLED_OAUTH_PROVIDERS_SQL)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
rows.iter().map(map_oauth_row).collect()
|
||||
}
|
||||
|
||||
async fn get_ldap_config(&self) -> Result<Option<StoredLdapModuleConfig>, DataLayerError> {
|
||||
get_ldap_config(&self.pool).await
|
||||
let row = sqlx::query(GET_LDAP_CONFIG_SQL)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
row.as_ref().map(map_ldap_row).transpose()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,11 +88,19 @@ impl AuthModuleReadRepository for MysqlAuthModuleRepository {
|
||||
async fn list_enabled_oauth_providers(
|
||||
&self,
|
||||
) -> Result<Vec<StoredOAuthProviderModuleConfig>, DataLayerError> {
|
||||
list_enabled_oauth_providers(&self.pool).await
|
||||
let rows = sqlx::query(LIST_ENABLED_OAUTH_PROVIDERS_SQL)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
rows.iter().map(map_oauth_row).collect()
|
||||
}
|
||||
|
||||
async fn get_ldap_config(&self) -> Result<Option<StoredLdapModuleConfig>, DataLayerError> {
|
||||
get_ldap_config(&self.pool).await
|
||||
let row = sqlx::query(GET_LDAP_CONFIG_SQL)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
row.as_ref().map(map_ldap_row).transpose()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,9 +10,6 @@ use super::{
|
||||
use crate::driver::mysql::MysqlPool;
|
||||
use crate::error::SqlResultExt;
|
||||
use crate::DataLayerError;
|
||||
use aether_data_query::{
|
||||
push_ci_contains, push_eq, push_limit, push_limit_offset, SqlDialect, WhereClause,
|
||||
};
|
||||
|
||||
const RUN_COLUMNS: &str = r#"
|
||||
SELECT
|
||||
@@ -60,29 +57,44 @@ impl MysqlBackgroundTaskRepository {
|
||||
}
|
||||
|
||||
fn apply_run_filter(builder: &mut QueryBuilder<'_, MySql>, query: &BackgroundTaskListQuery) {
|
||||
let mut where_clause = WhereClause::new();
|
||||
let mut has_where = false;
|
||||
if let Some(kind) = query.kind {
|
||||
push_eq(builder, &mut where_clause, "kind", kind.as_database());
|
||||
if !has_where {
|
||||
builder.push(" WHERE ");
|
||||
has_where = true;
|
||||
} else {
|
||||
builder.push(" AND ");
|
||||
}
|
||||
builder.push("kind = ").push_bind(kind.as_database());
|
||||
}
|
||||
if let Some(status) = query.status {
|
||||
push_eq(builder, &mut where_clause, "status", status.as_database());
|
||||
if !has_where {
|
||||
builder.push(" WHERE ");
|
||||
has_where = true;
|
||||
} else {
|
||||
builder.push(" AND ");
|
||||
}
|
||||
builder.push("status = ").push_bind(status.as_database());
|
||||
}
|
||||
if let Some(trigger) = query.trigger.as_deref() {
|
||||
push_eq(
|
||||
builder,
|
||||
&mut where_clause,
|
||||
&SqlDialect::Mysql.quote_ident("trigger"),
|
||||
trigger.to_string(),
|
||||
);
|
||||
if !has_where {
|
||||
builder.push(" WHERE ");
|
||||
has_where = true;
|
||||
} else {
|
||||
builder.push(" AND ");
|
||||
}
|
||||
builder.push("`trigger` = ").push_bind(trigger.to_string());
|
||||
}
|
||||
if let Some(task_key_substring) = query.task_key_substring.as_deref() {
|
||||
push_ci_contains(
|
||||
builder,
|
||||
&mut where_clause,
|
||||
SqlDialect::Mysql,
|
||||
"task_key",
|
||||
task_key_substring,
|
||||
);
|
||||
if !has_where {
|
||||
builder.push(" WHERE ");
|
||||
} else {
|
||||
builder.push(" AND ");
|
||||
}
|
||||
builder.push("LOWER(task_key) LIKE ").push_bind(format!(
|
||||
"%{}%",
|
||||
task_key_substring.trim().to_ascii_lowercase()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,12 +105,8 @@ impl BackgroundTaskReadRepository for MysqlBackgroundTaskRepository {
|
||||
&self,
|
||||
run_id: &str,
|
||||
) -> Result<Option<StoredBackgroundTaskRun>, DataLayerError> {
|
||||
let mut builder = QueryBuilder::<MySql>::new(RUN_COLUMNS);
|
||||
let mut where_clause = WhereClause::new();
|
||||
push_eq(&mut builder, &mut where_clause, "id", run_id.to_string());
|
||||
push_limit(&mut builder, 1);
|
||||
let row = builder
|
||||
.build()
|
||||
let row = sqlx::query(&format!("{RUN_COLUMNS} WHERE id = ? LIMIT 1"))
|
||||
.bind(run_id)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
@@ -121,12 +129,12 @@ impl BackgroundTaskReadRepository for MysqlBackgroundTaskRepository {
|
||||
|
||||
let mut builder = QueryBuilder::<MySql>::new(RUN_COLUMNS);
|
||||
Self::apply_run_filter(&mut builder, query);
|
||||
builder.push(" ORDER BY created_at_unix_secs DESC, updated_at_unix_secs DESC");
|
||||
push_limit_offset(
|
||||
&mut builder,
|
||||
i64_from_usize(limit, "run limit")?,
|
||||
i64_from_usize(query.offset, "run offset")?,
|
||||
);
|
||||
builder
|
||||
.push(" ORDER BY created_at_unix_secs DESC, updated_at_unix_secs DESC")
|
||||
.push(" LIMIT ")
|
||||
.push_bind(i64_from_usize(limit, "run limit")?)
|
||||
.push(" OFFSET ")
|
||||
.push_bind(i64_from_usize(query.offset, "run offset")?);
|
||||
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
||||
let items = rows
|
||||
.iter()
|
||||
@@ -145,21 +153,15 @@ impl BackgroundTaskReadRepository for MysqlBackgroundTaskRepository {
|
||||
limit: usize,
|
||||
) -> Result<Vec<StoredBackgroundTaskEvent>, DataLayerError> {
|
||||
let limit = limit.max(1);
|
||||
let mut builder = QueryBuilder::<MySql>::new(EVENT_COLUMNS);
|
||||
let mut where_clause = WhereClause::new();
|
||||
push_eq(
|
||||
&mut builder,
|
||||
&mut where_clause,
|
||||
"run_id",
|
||||
run_id.to_string(),
|
||||
);
|
||||
builder.push(" ORDER BY created_at_unix_secs ASC, id ASC");
|
||||
push_limit_offset(
|
||||
&mut builder,
|
||||
i64_from_usize(limit, "event limit")?,
|
||||
i64_from_usize(offset, "event offset")?,
|
||||
);
|
||||
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
||||
let rows = sqlx::query(&format!(
|
||||
"{EVENT_COLUMNS} WHERE run_id = ? ORDER BY created_at_unix_secs ASC, id ASC LIMIT ? OFFSET ?"
|
||||
))
|
||||
.bind(run_id)
|
||||
.bind(i64_from_usize(limit, "event limit")?)
|
||||
.bind(i64_from_usize(offset, "event offset")?)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
rows.iter().map(map_event_row).collect()
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ use super::{
|
||||
use crate::driver::mysql::MysqlPool;
|
||||
use crate::error::SqlResultExt;
|
||||
use crate::DataLayerError;
|
||||
use aether_data_query::{push_in, WhereClause};
|
||||
|
||||
const CANDIDATE_COLUMNS: &str = r#"
|
||||
SELECT
|
||||
@@ -133,8 +132,7 @@ impl RequestCandidateReadRepository for MysqlRequestCandidateRepository {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let mut builder = QueryBuilder::<MySql>::new(CANDIDATE_COLUMNS);
|
||||
let mut where_clause = WhereClause::new();
|
||||
push_in(&mut builder, &mut where_clause, "endpoint_id", endpoint_ids);
|
||||
push_endpoint_in_clause(&mut builder, endpoint_ids);
|
||||
builder
|
||||
.push(" AND created_at >= ")
|
||||
.push_bind(unix_secs_to_ms_i64(since_unix_secs)?)
|
||||
@@ -156,8 +154,7 @@ impl RequestCandidateReadRepository for MysqlRequestCandidateRepository {
|
||||
let mut builder = QueryBuilder::<MySql>::new(
|
||||
"SELECT endpoint_id, status, COUNT(id) AS count FROM request_candidates",
|
||||
);
|
||||
let mut where_clause = WhereClause::new();
|
||||
push_in(&mut builder, &mut where_clause, "endpoint_id", endpoint_ids);
|
||||
push_endpoint_in_clause(&mut builder, endpoint_ids);
|
||||
builder
|
||||
.push(" AND created_at >= ")
|
||||
.push_bind(unix_secs_to_ms_i64(since_unix_secs)?)
|
||||
@@ -196,8 +193,7 @@ impl RequestCandidateReadRepository for MysqlRequestCandidateRepository {
|
||||
let since_ms = unix_secs_to_ms_i64(since_unix_secs)?;
|
||||
let until_ms = unix_secs_to_ms_i64(until_unix_secs)?;
|
||||
let mut builder = QueryBuilder::<MySql>::new(CANDIDATE_COLUMNS);
|
||||
let mut where_clause = WhereClause::new();
|
||||
push_in(&mut builder, &mut where_clause, "endpoint_id", endpoint_ids);
|
||||
push_endpoint_in_clause(&mut builder, endpoint_ids);
|
||||
builder
|
||||
.push(" AND created_at >= ")
|
||||
.push_bind(since_ms)
|
||||
@@ -343,6 +339,20 @@ ON DUPLICATE KEY UPDATE
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn push_endpoint_in_clause<'args>(
|
||||
builder: &mut QueryBuilder<'args, MySql>,
|
||||
endpoint_ids: &'args [String],
|
||||
) {
|
||||
builder.push(" WHERE endpoint_id IN (");
|
||||
{
|
||||
let mut separated = builder.separated(", ");
|
||||
for endpoint_id in endpoint_ids {
|
||||
separated.push_bind(endpoint_id);
|
||||
}
|
||||
}
|
||||
builder.push(")");
|
||||
}
|
||||
|
||||
fn merge_candidate(
|
||||
candidate: UpsertRequestCandidateRecord,
|
||||
existing: Option<StoredRequestCandidate>,
|
||||
|
||||
@@ -9,7 +9,6 @@ use super::types::{
|
||||
use crate::driver::mysql::MysqlPool;
|
||||
use crate::error::SqlResultExt;
|
||||
use crate::DataLayerError;
|
||||
use aether_data_query::{push_ci_contains_any, push_limit_offset, SqlDialect, WhereClause};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MysqlGeminiFileMappingRepository {
|
||||
@@ -243,9 +242,8 @@ LIMIT 1
|
||||
|
||||
fn build_list_count_query(query: &GeminiFileMappingListQuery) -> QueryBuilder<'_, MySql> {
|
||||
let mut builder =
|
||||
QueryBuilder::<MySql>::new("SELECT COUNT(*) AS total FROM gemini_file_mappings");
|
||||
let mut where_clause = WhereClause::new();
|
||||
apply_list_filters(&mut builder, &mut where_clause, query);
|
||||
QueryBuilder::<MySql>::new("SELECT COUNT(*) AS total FROM gemini_file_mappings WHERE 1=1");
|
||||
apply_list_filters(&mut builder, query);
|
||||
builder
|
||||
}
|
||||
|
||||
@@ -263,27 +261,20 @@ SELECT
|
||||
created_at AS created_at_unix_ms,
|
||||
expires_at AS expires_at_unix_secs
|
||||
FROM gemini_file_mappings
|
||||
WHERE 1=1
|
||||
"#,
|
||||
);
|
||||
let mut where_clause = WhereClause::new();
|
||||
apply_list_filters(&mut builder, &mut where_clause, query);
|
||||
builder.push(" ORDER BY created_at DESC, file_name ASC");
|
||||
push_limit_offset(
|
||||
&mut builder,
|
||||
i64::try_from(query.limit).unwrap_or(i64::MAX),
|
||||
i64::try_from(query.offset).unwrap_or(i64::MAX),
|
||||
);
|
||||
apply_list_filters(&mut builder, query);
|
||||
builder.push(" ORDER BY created_at DESC, file_name ASC LIMIT ");
|
||||
builder.push_bind(i64::try_from(query.limit).unwrap_or(i64::MAX));
|
||||
builder.push(" OFFSET ");
|
||||
builder.push_bind(i64::try_from(query.offset).unwrap_or(i64::MAX));
|
||||
builder
|
||||
}
|
||||
|
||||
fn apply_list_filters(
|
||||
builder: &mut QueryBuilder<'_, MySql>,
|
||||
where_clause: &mut WhereClause,
|
||||
query: &GeminiFileMappingListQuery,
|
||||
) {
|
||||
fn apply_list_filters(builder: &mut QueryBuilder<'_, MySql>, query: &GeminiFileMappingListQuery) {
|
||||
if !query.include_expired {
|
||||
where_clause.push_next(builder);
|
||||
builder.push("expires_at > ");
|
||||
builder.push(" AND expires_at > ");
|
||||
builder.push_bind(query.now_unix_secs as i64);
|
||||
}
|
||||
if let Some(search) = query
|
||||
@@ -292,13 +283,12 @@ fn apply_list_filters(
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
push_ci_contains_any(
|
||||
builder,
|
||||
where_clause,
|
||||
SqlDialect::Mysql,
|
||||
&["file_name", "COALESCE(display_name, '')"],
|
||||
search,
|
||||
);
|
||||
let pattern = format!("%{}%", search.to_ascii_lowercase());
|
||||
builder.push(" AND (LOWER(file_name) LIKE ");
|
||||
builder.push_bind(pattern.clone());
|
||||
builder.push(" OR LOWER(COALESCE(display_name, '')) LIKE ");
|
||||
builder.push_bind(pattern);
|
||||
builder.push(")");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use async_trait::async_trait;
|
||||
use sqlx::{mysql::MySqlRow, MySql, QueryBuilder, Row};
|
||||
use sqlx::{mysql::MySqlRow, Row};
|
||||
|
||||
use super::types::{
|
||||
CreateManagementTokenRecord, ManagementTokenListQuery, ManagementTokenReadRepository,
|
||||
@@ -10,7 +10,6 @@ use super::types::{
|
||||
use crate::driver::mysql::MysqlPool;
|
||||
use crate::error::SqlResultExt;
|
||||
use crate::DataLayerError;
|
||||
use aether_data_query::{push_eq, push_limit, push_limit_offset, push_optional_eq, WhereClause};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MysqlManagementTokenRepository {
|
||||
@@ -26,12 +25,8 @@ impl MysqlManagementTokenRepository {
|
||||
&self,
|
||||
token_id: &str,
|
||||
) -> Result<Option<StoredManagementToken>, DataLayerError> {
|
||||
let mut builder = QueryBuilder::<MySql>::new(TOKEN_COLUMNS);
|
||||
let mut where_clause = WhereClause::new();
|
||||
push_eq(&mut builder, &mut where_clause, "id", token_id.to_string());
|
||||
push_limit(&mut builder, 1);
|
||||
let row = builder
|
||||
.build()
|
||||
let row = sqlx::query(TOKEN_BY_ID_SQL)
|
||||
.bind(token_id)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
@@ -39,7 +34,7 @@ impl MysqlManagementTokenRepository {
|
||||
}
|
||||
}
|
||||
|
||||
const TOKEN_COLUMNS: &str = r#"
|
||||
const TOKEN_BY_ID_SQL: &str = r#"
|
||||
SELECT
|
||||
id,
|
||||
user_id,
|
||||
@@ -56,9 +51,11 @@ SELECT
|
||||
created_at AS created_at_unix_ms,
|
||||
updated_at AS updated_at_unix_secs
|
||||
FROM management_tokens
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
"#;
|
||||
|
||||
const TOKEN_WITH_USER_COLUMNS: &str = r#"
|
||||
const LIST_MANAGEMENT_TOKENS_SQL: &str = r#"
|
||||
SELECT
|
||||
mt.id,
|
||||
mt.user_id,
|
||||
@@ -80,6 +77,69 @@ SELECT
|
||||
u.role AS user_role
|
||||
FROM management_tokens mt
|
||||
JOIN users u ON u.id = mt.user_id
|
||||
WHERE (? IS NULL OR mt.user_id = ?)
|
||||
AND (? IS NULL OR mt.is_active = ?)
|
||||
ORDER BY mt.created_at DESC, mt.id DESC
|
||||
LIMIT ? OFFSET ?
|
||||
"#;
|
||||
|
||||
const COUNT_MANAGEMENT_TOKENS_SQL: &str = r#"
|
||||
SELECT COUNT(mt.id) AS total
|
||||
FROM management_tokens mt
|
||||
WHERE (? IS NULL OR mt.user_id = ?)
|
||||
AND (? IS NULL OR mt.is_active = ?)
|
||||
"#;
|
||||
|
||||
const GET_MANAGEMENT_TOKEN_WITH_USER_SQL: &str = r#"
|
||||
SELECT
|
||||
mt.id,
|
||||
mt.user_id,
|
||||
mt.name,
|
||||
mt.description,
|
||||
mt.token_prefix,
|
||||
mt.allowed_ips,
|
||||
mt.permissions,
|
||||
mt.expires_at AS expires_at_unix_secs,
|
||||
mt.last_used_at AS last_used_at_unix_secs,
|
||||
mt.last_used_ip,
|
||||
COALESCE(mt.usage_count, 0) AS usage_count,
|
||||
mt.is_active,
|
||||
mt.created_at AS created_at_unix_ms,
|
||||
mt.updated_at AS updated_at_unix_secs,
|
||||
u.id AS user_row_id,
|
||||
u.email AS user_email,
|
||||
u.username AS user_username,
|
||||
u.role AS user_role
|
||||
FROM management_tokens mt
|
||||
JOIN users u ON u.id = mt.user_id
|
||||
WHERE mt.id = ?
|
||||
LIMIT 1
|
||||
"#;
|
||||
|
||||
const GET_MANAGEMENT_TOKEN_WITH_USER_BY_HASH_SQL: &str = r#"
|
||||
SELECT
|
||||
mt.id,
|
||||
mt.user_id,
|
||||
mt.name,
|
||||
mt.description,
|
||||
mt.token_prefix,
|
||||
mt.allowed_ips,
|
||||
mt.permissions,
|
||||
mt.expires_at AS expires_at_unix_secs,
|
||||
mt.last_used_at AS last_used_at_unix_secs,
|
||||
mt.last_used_ip,
|
||||
COALESCE(mt.usage_count, 0) AS usage_count,
|
||||
mt.is_active,
|
||||
mt.created_at AS created_at_unix_ms,
|
||||
mt.updated_at AS updated_at_unix_secs,
|
||||
u.id AS user_row_id,
|
||||
u.email AS user_email,
|
||||
u.username AS user_username,
|
||||
u.role AS user_role
|
||||
FROM management_tokens mt
|
||||
JOIN users u ON u.id = mt.user_id
|
||||
WHERE mt.token_hash = ?
|
||||
LIMIT 1
|
||||
"#;
|
||||
|
||||
#[async_trait]
|
||||
@@ -88,27 +148,23 @@ impl ManagementTokenReadRepository for MysqlManagementTokenRepository {
|
||||
&self,
|
||||
query: &ManagementTokenListQuery,
|
||||
) -> Result<StoredManagementTokenListPage, DataLayerError> {
|
||||
let mut count_builder =
|
||||
QueryBuilder::<MySql>::new("SELECT COUNT(mt.id) AS total FROM management_tokens mt");
|
||||
let mut count_where = WhereClause::new();
|
||||
apply_management_token_filters(&mut count_builder, &mut count_where, query);
|
||||
let total = count_builder
|
||||
.build_query_scalar::<i64>()
|
||||
let count_row = sqlx::query(COUNT_MANAGEMENT_TOKENS_SQL)
|
||||
.bind(query.user_id.as_deref())
|
||||
.bind(query.user_id.as_deref())
|
||||
.bind(query.is_active)
|
||||
.bind(query.is_active)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
let total = count_row.try_get::<i64, _>("total").map_sql_err()?;
|
||||
|
||||
let mut list_builder = QueryBuilder::<MySql>::new(TOKEN_WITH_USER_COLUMNS);
|
||||
let mut list_where = WhereClause::new();
|
||||
apply_management_token_filters(&mut list_builder, &mut list_where, query);
|
||||
list_builder.push(" ORDER BY mt.created_at DESC, mt.id DESC");
|
||||
push_limit_offset(
|
||||
&mut list_builder,
|
||||
i64::try_from(query.limit).unwrap_or(i64::MAX),
|
||||
i64::try_from(query.offset).unwrap_or(i64::MAX),
|
||||
);
|
||||
let rows = list_builder
|
||||
.build()
|
||||
let rows = sqlx::query(LIST_MANAGEMENT_TOKENS_SQL)
|
||||
.bind(query.user_id.as_deref())
|
||||
.bind(query.user_id.as_deref())
|
||||
.bind(query.is_active)
|
||||
.bind(query.is_active)
|
||||
.bind(i64::try_from(query.limit).unwrap_or(i64::MAX))
|
||||
.bind(i64::try_from(query.offset).unwrap_or(i64::MAX))
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
@@ -126,17 +182,8 @@ impl ManagementTokenReadRepository for MysqlManagementTokenRepository {
|
||||
&self,
|
||||
token_id: &str,
|
||||
) -> Result<Option<StoredManagementTokenWithUser>, DataLayerError> {
|
||||
let mut builder = QueryBuilder::<MySql>::new(TOKEN_WITH_USER_COLUMNS);
|
||||
let mut where_clause = WhereClause::new();
|
||||
push_eq(
|
||||
&mut builder,
|
||||
&mut where_clause,
|
||||
"mt.id",
|
||||
token_id.to_string(),
|
||||
);
|
||||
push_limit(&mut builder, 1);
|
||||
let row = builder
|
||||
.build()
|
||||
let row = sqlx::query(GET_MANAGEMENT_TOKEN_WITH_USER_SQL)
|
||||
.bind(token_id)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
@@ -147,17 +194,8 @@ impl ManagementTokenReadRepository for MysqlManagementTokenRepository {
|
||||
&self,
|
||||
token_hash: &str,
|
||||
) -> Result<Option<StoredManagementTokenWithUser>, DataLayerError> {
|
||||
let mut builder = QueryBuilder::<MySql>::new(TOKEN_WITH_USER_COLUMNS);
|
||||
let mut where_clause = WhereClause::new();
|
||||
push_eq(
|
||||
&mut builder,
|
||||
&mut where_clause,
|
||||
"mt.token_hash",
|
||||
token_hash.to_string(),
|
||||
);
|
||||
push_limit(&mut builder, 1);
|
||||
let row = builder
|
||||
.build()
|
||||
let row = sqlx::query(GET_MANAGEMENT_TOKEN_WITH_USER_BY_HASH_SQL)
|
||||
.bind(token_hash)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
@@ -165,15 +203,6 @@ impl ManagementTokenReadRepository for MysqlManagementTokenRepository {
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_management_token_filters(
|
||||
builder: &mut QueryBuilder<'_, MySql>,
|
||||
where_clause: &mut WhereClause,
|
||||
query: &ManagementTokenListQuery,
|
||||
) {
|
||||
push_optional_eq(builder, where_clause, "mt.user_id", query.user_id.clone());
|
||||
push_optional_eq(builder, where_clause, "mt.is_active", query.is_active);
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ManagementTokenWriteRepository for MysqlManagementTokenRepository {
|
||||
async fn create_management_token(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use async_trait::async_trait;
|
||||
use sqlx::{mysql::MySqlRow, MySql, QueryBuilder, Row};
|
||||
use sqlx::{mysql::MySqlRow, Row};
|
||||
|
||||
use super::types::{
|
||||
OAuthProviderReadRepository, OAuthProviderWriteRepository, StoredOAuthProviderConfig,
|
||||
@@ -8,7 +8,6 @@ use super::types::{
|
||||
use crate::driver::mysql::MysqlPool;
|
||||
use crate::error::SqlResultExt;
|
||||
use crate::DataLayerError;
|
||||
use aether_data_query::{push_eq, push_limit, WhereClause};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MysqlOAuthProviderRepository {
|
||||
@@ -24,17 +23,8 @@ impl MysqlOAuthProviderRepository {
|
||||
&self,
|
||||
provider_type: &str,
|
||||
) -> Result<Option<StoredOAuthProviderConfig>, DataLayerError> {
|
||||
let mut builder = QueryBuilder::<MySql>::new(OAUTH_PROVIDER_COLUMNS);
|
||||
let mut where_clause = WhereClause::new();
|
||||
push_eq(
|
||||
&mut builder,
|
||||
&mut where_clause,
|
||||
"provider_type",
|
||||
provider_type.to_string(),
|
||||
);
|
||||
push_limit(&mut builder, 1);
|
||||
let row = builder
|
||||
.build()
|
||||
let row = sqlx::query(GET_OAUTH_PROVIDER_CONFIG_SQL)
|
||||
.bind(provider_type)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
@@ -42,7 +32,7 @@ impl MysqlOAuthProviderRepository {
|
||||
}
|
||||
}
|
||||
|
||||
const OAUTH_PROVIDER_COLUMNS: &str = r#"
|
||||
const LIST_OAUTH_PROVIDER_CONFIGS_SQL: &str = r#"
|
||||
SELECT
|
||||
provider_type,
|
||||
display_name,
|
||||
@@ -60,6 +50,29 @@ SELECT
|
||||
created_at AS created_at_unix_ms,
|
||||
updated_at AS updated_at_unix_secs
|
||||
FROM oauth_providers
|
||||
ORDER BY provider_type ASC
|
||||
"#;
|
||||
|
||||
const GET_OAUTH_PROVIDER_CONFIG_SQL: &str = r#"
|
||||
SELECT
|
||||
provider_type,
|
||||
display_name,
|
||||
client_id,
|
||||
client_secret_encrypted,
|
||||
authorization_url_override,
|
||||
token_url_override,
|
||||
userinfo_url_override,
|
||||
scopes,
|
||||
redirect_uri,
|
||||
frontend_callback_url,
|
||||
attribute_mapping,
|
||||
extra_config,
|
||||
is_enabled,
|
||||
created_at AS created_at_unix_ms,
|
||||
updated_at AS updated_at_unix_secs
|
||||
FROM oauth_providers
|
||||
WHERE provider_type = ?
|
||||
LIMIT 1
|
||||
"#;
|
||||
|
||||
const COUNT_LOCKED_USERS_IF_PROVIDER_DISABLED_SQL: &str = r#"
|
||||
@@ -104,9 +117,10 @@ impl OAuthProviderReadRepository for MysqlOAuthProviderRepository {
|
||||
async fn list_oauth_provider_configs(
|
||||
&self,
|
||||
) -> Result<Vec<StoredOAuthProviderConfig>, DataLayerError> {
|
||||
let mut builder = QueryBuilder::<MySql>::new(OAUTH_PROVIDER_COLUMNS);
|
||||
builder.push(" ORDER BY provider_type ASC");
|
||||
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
||||
let rows = sqlx::query(LIST_OAUTH_PROVIDER_CONFIGS_SQL)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
rows.iter().map(map_oauth_provider_row).collect()
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ use super::{
|
||||
use crate::error::SqlResultExt;
|
||||
use crate::repository::pool_scores::merge_score_reason_patch;
|
||||
use crate::DataLayerError;
|
||||
use aether_data_query::{push_eq, push_in, push_limit, push_limit_offset, WhereClause};
|
||||
|
||||
const SCORE_COLUMNS: &str = r#"
|
||||
SELECT
|
||||
@@ -58,54 +57,25 @@ impl MysqlPoolMemberScoreRepository {
|
||||
scope: Option<&PoolScoreScope>,
|
||||
) -> Result<Vec<StoredPoolMemberScore>, DataLayerError> {
|
||||
let mut builder = QueryBuilder::<MySql>::new(SCORE_COLUMNS);
|
||||
let mut where_clause = WhereClause::new();
|
||||
push_eq(
|
||||
&mut builder,
|
||||
&mut where_clause,
|
||||
"pool_kind",
|
||||
identity.pool_kind.clone(),
|
||||
);
|
||||
push_eq(
|
||||
&mut builder,
|
||||
&mut where_clause,
|
||||
"pool_id",
|
||||
identity.pool_id.clone(),
|
||||
);
|
||||
push_eq(
|
||||
&mut builder,
|
||||
&mut where_clause,
|
||||
"member_kind",
|
||||
identity.member_kind.clone(),
|
||||
);
|
||||
push_eq(
|
||||
&mut builder,
|
||||
&mut where_clause,
|
||||
"member_id",
|
||||
identity.member_id.clone(),
|
||||
);
|
||||
builder
|
||||
.push(" WHERE pool_kind = ")
|
||||
.push_bind(identity.pool_kind.clone())
|
||||
.push(" AND pool_id = ")
|
||||
.push_bind(identity.pool_id.clone())
|
||||
.push(" AND member_kind = ")
|
||||
.push_bind(identity.member_kind.clone())
|
||||
.push(" AND member_id = ")
|
||||
.push_bind(identity.member_id.clone());
|
||||
if let Some(scope) = scope {
|
||||
push_eq(
|
||||
&mut builder,
|
||||
&mut where_clause,
|
||||
"capability",
|
||||
scope.capability.clone(),
|
||||
);
|
||||
push_eq(
|
||||
&mut builder,
|
||||
&mut where_clause,
|
||||
"scope_kind",
|
||||
scope.scope_kind.clone(),
|
||||
);
|
||||
builder
|
||||
.push(" AND capability = ")
|
||||
.push_bind(scope.capability.clone())
|
||||
.push(" AND scope_kind = ")
|
||||
.push_bind(scope.scope_kind.clone());
|
||||
if let Some(scope_id) = &scope.scope_id {
|
||||
push_eq(
|
||||
&mut builder,
|
||||
&mut where_clause,
|
||||
"scope_id",
|
||||
scope_id.clone(),
|
||||
);
|
||||
builder.push(" AND scope_id = ").push_bind(scope_id.clone());
|
||||
} else {
|
||||
where_clause.push_next(&mut builder);
|
||||
builder.push("scope_id IS NULL");
|
||||
builder.push(" AND scope_id IS NULL");
|
||||
}
|
||||
}
|
||||
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
||||
@@ -120,65 +90,44 @@ impl PoolScoreReadRepository for MysqlPoolMemberScoreRepository {
|
||||
query: &ListRankedPoolMembersQuery,
|
||||
) -> Result<Vec<StoredPoolMemberScore>, DataLayerError> {
|
||||
let mut builder = QueryBuilder::<MySql>::new(SCORE_COLUMNS);
|
||||
let mut where_clause = WhereClause::new();
|
||||
push_eq(
|
||||
&mut builder,
|
||||
&mut where_clause,
|
||||
"pool_kind",
|
||||
query.pool_kind.clone(),
|
||||
);
|
||||
push_eq(
|
||||
&mut builder,
|
||||
&mut where_clause,
|
||||
"pool_id",
|
||||
query.pool_id.clone(),
|
||||
);
|
||||
push_eq(
|
||||
&mut builder,
|
||||
&mut where_clause,
|
||||
"capability",
|
||||
query.capability.clone(),
|
||||
);
|
||||
push_eq(
|
||||
&mut builder,
|
||||
&mut where_clause,
|
||||
"scope_kind",
|
||||
query.scope_kind.clone(),
|
||||
);
|
||||
builder
|
||||
.push(" WHERE pool_kind = ")
|
||||
.push_bind(query.pool_kind.clone())
|
||||
.push(" AND pool_id = ")
|
||||
.push_bind(query.pool_id.clone())
|
||||
.push(" AND capability = ")
|
||||
.push_bind(query.capability.clone())
|
||||
.push(" AND scope_kind = ")
|
||||
.push_bind(query.scope_kind.clone());
|
||||
if let Some(scope_id) = &query.scope_id {
|
||||
push_eq(
|
||||
&mut builder,
|
||||
&mut where_clause,
|
||||
"scope_id",
|
||||
scope_id.clone(),
|
||||
);
|
||||
builder.push(" AND scope_id = ").push_bind(scope_id.clone());
|
||||
} else {
|
||||
where_clause.push_next(&mut builder);
|
||||
builder.push("scope_id IS NULL");
|
||||
builder.push(" AND scope_id IS NULL");
|
||||
}
|
||||
if !query.hard_states.is_empty() {
|
||||
let states = query
|
||||
.hard_states
|
||||
.iter()
|
||||
.map(|state| state.as_database())
|
||||
.collect::<Vec<_>>();
|
||||
push_in(&mut builder, &mut where_clause, "hard_state", &states);
|
||||
builder.push(" AND hard_state IN (");
|
||||
let mut separated = builder.separated(", ");
|
||||
for state in &query.hard_states {
|
||||
separated.push_bind(state.as_database());
|
||||
}
|
||||
separated.push_unseparated(")");
|
||||
}
|
||||
if let Some(statuses) = &query.probe_statuses {
|
||||
if !statuses.is_empty() {
|
||||
let statuses = statuses
|
||||
.iter()
|
||||
.map(|status| status.as_database())
|
||||
.collect::<Vec<_>>();
|
||||
push_in(&mut builder, &mut where_clause, "probe_status", &statuses);
|
||||
builder.push(" AND probe_status IN (");
|
||||
let mut separated = builder.separated(", ");
|
||||
for status in statuses {
|
||||
separated.push_bind(status.as_database());
|
||||
}
|
||||
separated.push_unseparated(")");
|
||||
}
|
||||
}
|
||||
builder.push(" ORDER BY score DESC, last_ranked_at DESC, member_id ASC, id ASC");
|
||||
push_limit_offset(
|
||||
&mut builder,
|
||||
i64_from_usize(query.limit.max(1), "pool score limit")?,
|
||||
i64_from_usize(query.offset, "pool score offset")?,
|
||||
);
|
||||
builder
|
||||
.push(" ORDER BY score DESC, last_ranked_at DESC, member_id ASC, id ASC")
|
||||
.push(" LIMIT ")
|
||||
.push_bind(i64_from_usize(query.limit.max(1), "pool score limit")?)
|
||||
.push(" OFFSET ")
|
||||
.push_bind(i64_from_usize(query.offset, "pool score offset")?);
|
||||
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
||||
rows.iter().map(map_score_row).collect()
|
||||
}
|
||||
@@ -188,66 +137,48 @@ impl PoolScoreReadRepository for MysqlPoolMemberScoreRepository {
|
||||
query: &ListPoolMemberScoresQuery,
|
||||
) -> Result<Vec<StoredPoolMemberScore>, DataLayerError> {
|
||||
let mut builder = QueryBuilder::<MySql>::new(SCORE_COLUMNS);
|
||||
let mut where_clause = WhereClause::new();
|
||||
push_eq(
|
||||
&mut builder,
|
||||
&mut where_clause,
|
||||
"pool_kind",
|
||||
query.pool_kind.clone(),
|
||||
);
|
||||
push_eq(
|
||||
&mut builder,
|
||||
&mut where_clause,
|
||||
"pool_id",
|
||||
query.pool_id.clone(),
|
||||
);
|
||||
builder
|
||||
.push(" WHERE pool_kind = ")
|
||||
.push_bind(query.pool_kind.clone())
|
||||
.push(" AND pool_id = ")
|
||||
.push_bind(query.pool_id.clone());
|
||||
if let Some(capability) = &query.capability {
|
||||
push_eq(
|
||||
&mut builder,
|
||||
&mut where_clause,
|
||||
"capability",
|
||||
capability.clone(),
|
||||
);
|
||||
builder
|
||||
.push(" AND capability = ")
|
||||
.push_bind(capability.clone());
|
||||
}
|
||||
if let Some(scope_kind) = &query.scope_kind {
|
||||
push_eq(
|
||||
&mut builder,
|
||||
&mut where_clause,
|
||||
"scope_kind",
|
||||
scope_kind.clone(),
|
||||
);
|
||||
builder
|
||||
.push(" AND scope_kind = ")
|
||||
.push_bind(scope_kind.clone());
|
||||
}
|
||||
if let Some(scope_id) = &query.scope_id {
|
||||
push_eq(
|
||||
&mut builder,
|
||||
&mut where_clause,
|
||||
"scope_id",
|
||||
scope_id.clone(),
|
||||
);
|
||||
builder.push(" AND scope_id = ").push_bind(scope_id.clone());
|
||||
}
|
||||
if !query.hard_states.is_empty() {
|
||||
let states = query
|
||||
.hard_states
|
||||
.iter()
|
||||
.map(|state| state.as_database())
|
||||
.collect::<Vec<_>>();
|
||||
push_in(&mut builder, &mut where_clause, "hard_state", &states);
|
||||
builder.push(" AND hard_state IN (");
|
||||
let mut separated = builder.separated(", ");
|
||||
for state in &query.hard_states {
|
||||
separated.push_bind(state.as_database());
|
||||
}
|
||||
separated.push_unseparated(")");
|
||||
}
|
||||
if let Some(statuses) = &query.probe_statuses {
|
||||
if !statuses.is_empty() {
|
||||
let statuses = statuses
|
||||
.iter()
|
||||
.map(|status| status.as_database())
|
||||
.collect::<Vec<_>>();
|
||||
push_in(&mut builder, &mut where_clause, "probe_status", &statuses);
|
||||
builder.push(" AND probe_status IN (");
|
||||
let mut separated = builder.separated(", ");
|
||||
for status in statuses {
|
||||
separated.push_bind(status.as_database());
|
||||
}
|
||||
separated.push_unseparated(")");
|
||||
}
|
||||
}
|
||||
builder.push(" ORDER BY score DESC, last_ranked_at DESC, member_id ASC, id ASC");
|
||||
push_limit_offset(
|
||||
&mut builder,
|
||||
i64_from_usize(query.limit.max(1), "pool score limit")?,
|
||||
i64_from_usize(query.offset, "pool score offset")?,
|
||||
);
|
||||
builder
|
||||
.push(" ORDER BY score DESC, last_ranked_at DESC, member_id ASC, id ASC")
|
||||
.push(" LIMIT ")
|
||||
.push_bind(i64_from_usize(query.limit.max(1), "pool score limit")?)
|
||||
.push(" OFFSET ")
|
||||
.push_bind(i64_from_usize(query.offset, "pool score offset")?);
|
||||
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
||||
rows.iter().map(map_score_row).collect()
|
||||
}
|
||||
@@ -257,30 +188,18 @@ impl PoolScoreReadRepository for MysqlPoolMemberScoreRepository {
|
||||
query: &ListPoolMemberProbeCandidatesQuery,
|
||||
) -> Result<Vec<StoredPoolMemberScore>, DataLayerError> {
|
||||
let mut builder = QueryBuilder::<MySql>::new(SCORE_COLUMNS);
|
||||
let mut where_clause = WhereClause::new();
|
||||
push_eq(
|
||||
&mut builder,
|
||||
&mut where_clause,
|
||||
"pool_kind",
|
||||
query.pool_kind.clone(),
|
||||
);
|
||||
push_eq(
|
||||
&mut builder,
|
||||
&mut where_clause,
|
||||
"pool_id",
|
||||
query.pool_id.clone(),
|
||||
);
|
||||
if let Some(capability) = &query.capability {
|
||||
push_eq(
|
||||
&mut builder,
|
||||
&mut where_clause,
|
||||
"capability",
|
||||
capability.clone(),
|
||||
);
|
||||
}
|
||||
where_clause.push_next(&mut builder);
|
||||
builder
|
||||
.push("hard_state IN ('available','unknown','cooldown','quota_exhausted')")
|
||||
.push(" WHERE pool_kind = ")
|
||||
.push_bind(query.pool_kind.clone())
|
||||
.push(" AND pool_id = ")
|
||||
.push_bind(query.pool_id.clone());
|
||||
if let Some(capability) = &query.capability {
|
||||
builder
|
||||
.push(" AND capability = ")
|
||||
.push_bind(capability.clone());
|
||||
}
|
||||
builder
|
||||
.push(" AND hard_state IN ('available','unknown','cooldown','quota_exhausted')")
|
||||
.push(" AND (probe_status IN ('never','failed','stale')")
|
||||
.push(" OR (probe_status = 'ok' AND (last_probe_success_at IS NULL OR last_probe_success_at <= ")
|
||||
.push_bind(i64_from_u64(
|
||||
@@ -309,11 +228,12 @@ impl PoolScoreReadRepository for MysqlPoolMemberScoreRepository {
|
||||
COALESCE(last_scheduled_at, 0) DESC,
|
||||
member_id ASC
|
||||
"#,
|
||||
);
|
||||
push_limit(
|
||||
&mut builder,
|
||||
i64_from_usize(query.limit.max(1), "pool probe candidate limit")?,
|
||||
);
|
||||
)
|
||||
.push(" LIMIT ")
|
||||
.push_bind(i64_from_usize(
|
||||
query.limit.max(1),
|
||||
"pool probe candidate limit",
|
||||
)?);
|
||||
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
||||
rows.iter().map(map_score_row).collect()
|
||||
}
|
||||
@@ -326,8 +246,12 @@ impl PoolScoreReadRepository for MysqlPoolMemberScoreRepository {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let mut builder = QueryBuilder::<MySql>::new(SCORE_COLUMNS);
|
||||
let mut where_clause = WhereClause::new();
|
||||
push_in(&mut builder, &mut where_clause, "id", &query.ids);
|
||||
builder.push(" WHERE id IN (");
|
||||
let mut separated = builder.separated(", ");
|
||||
for id in &query.ids {
|
||||
separated.push_bind(id.clone());
|
||||
}
|
||||
separated.push_unseparated(")");
|
||||
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
||||
rows.iter().map(map_score_row).collect()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use async_trait::async_trait;
|
||||
use sqlx::{mysql::MySqlRow, MySql, QueryBuilder, Row};
|
||||
use sqlx::{mysql::MySqlRow, Row};
|
||||
|
||||
use super::types::{
|
||||
bucket_start_unix_secs, build_tunnel_error_event_detail, build_tunnel_metrics_sample,
|
||||
@@ -14,7 +14,6 @@ use super::types::{
|
||||
use crate::driver::mysql::MysqlPool;
|
||||
use crate::error::SqlResultExt;
|
||||
use crate::DataLayerError;
|
||||
use aether_data_query::{push_eq, push_limit, WhereClause};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MysqlProxyNodeReadRepository {
|
||||
@@ -338,23 +337,13 @@ SELECT
|
||||
FROM proxy_nodes
|
||||
"#;
|
||||
|
||||
const PROXY_NODE_EVENT_COLUMNS: &str = r#"
|
||||
SELECT
|
||||
id,
|
||||
node_id,
|
||||
event_type,
|
||||
detail,
|
||||
event_metadata,
|
||||
created_at AS created_at_unix_ms
|
||||
FROM proxy_node_events
|
||||
"#;
|
||||
|
||||
#[async_trait]
|
||||
impl ProxyNodeReadRepository for MysqlProxyNodeReadRepository {
|
||||
async fn list_proxy_nodes(&self) -> Result<Vec<StoredProxyNode>, DataLayerError> {
|
||||
let mut builder = QueryBuilder::<MySql>::new(PROXY_NODE_COLUMNS);
|
||||
builder.push(" ORDER BY name ASC, id ASC");
|
||||
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
||||
let rows = sqlx::query(&format!("{PROXY_NODE_COLUMNS} ORDER BY name ASC, id ASC"))
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
rows.iter().map(map_proxy_node_row).collect()
|
||||
}
|
||||
|
||||
@@ -362,12 +351,8 @@ impl ProxyNodeReadRepository for MysqlProxyNodeReadRepository {
|
||||
&self,
|
||||
node_id: &str,
|
||||
) -> Result<Option<StoredProxyNode>, DataLayerError> {
|
||||
let mut builder = QueryBuilder::<MySql>::new(PROXY_NODE_COLUMNS);
|
||||
let mut where_clause = WhereClause::new();
|
||||
push_eq(&mut builder, &mut where_clause, "id", node_id.to_string());
|
||||
push_limit(&mut builder, 1);
|
||||
let row = builder
|
||||
.build()
|
||||
let row = sqlx::query(&format!("{PROXY_NODE_COLUMNS} WHERE id = ? LIMIT 1"))
|
||||
.bind(node_id)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
@@ -379,17 +364,26 @@ impl ProxyNodeReadRepository for MysqlProxyNodeReadRepository {
|
||||
node_id: &str,
|
||||
limit: usize,
|
||||
) -> Result<Vec<StoredProxyNodeEvent>, DataLayerError> {
|
||||
let mut builder = QueryBuilder::<MySql>::new(PROXY_NODE_EVENT_COLUMNS);
|
||||
let mut where_clause = WhereClause::new();
|
||||
push_eq(
|
||||
&mut builder,
|
||||
&mut where_clause,
|
||||
"node_id",
|
||||
node_id.to_string(),
|
||||
);
|
||||
builder.push(" ORDER BY created_at DESC, id DESC");
|
||||
push_limit(&mut builder, i64::try_from(limit).unwrap_or(i64::MAX));
|
||||
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
node_id,
|
||||
event_type,
|
||||
detail,
|
||||
event_metadata,
|
||||
created_at AS created_at_unix_ms
|
||||
FROM proxy_node_events
|
||||
WHERE node_id = ?
|
||||
ORDER BY created_at DESC, id DESC
|
||||
LIMIT ?
|
||||
"#,
|
||||
)
|
||||
.bind(node_id)
|
||||
.bind(i64::try_from(limit).unwrap_or(i64::MAX))
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
rows.iter().map(map_proxy_node_event_row).collect()
|
||||
}
|
||||
|
||||
@@ -398,36 +392,51 @@ impl ProxyNodeReadRepository for MysqlProxyNodeReadRepository {
|
||||
node_id: &str,
|
||||
query: &ProxyNodeEventQuery,
|
||||
) -> Result<Vec<StoredProxyNodeEvent>, DataLayerError> {
|
||||
let mut builder = QueryBuilder::<MySql>::new(PROXY_NODE_EVENT_COLUMNS);
|
||||
let mut where_clause = WhereClause::new();
|
||||
push_eq(
|
||||
&mut builder,
|
||||
&mut where_clause,
|
||||
"node_id",
|
||||
node_id.to_string(),
|
||||
);
|
||||
if let Some(from_unix_secs) = query.from_unix_secs {
|
||||
where_clause.push_next(&mut builder);
|
||||
builder
|
||||
.push("created_at >= ")
|
||||
.push_bind(i64::try_from(from_unix_secs).unwrap_or(i64::MAX));
|
||||
}
|
||||
if let Some(to_unix_secs) = query.to_unix_secs {
|
||||
where_clause.push_next(&mut builder);
|
||||
builder
|
||||
.push("created_at <= ")
|
||||
.push_bind(i64::try_from(to_unix_secs).unwrap_or(i64::MAX));
|
||||
}
|
||||
if let Some(event_type) = query.event_type.as_deref() {
|
||||
where_clause.push_next(&mut builder);
|
||||
builder
|
||||
.push("LOWER(event_type) = LOWER(")
|
||||
.push_bind(event_type.to_string())
|
||||
.push(")");
|
||||
}
|
||||
builder.push(" ORDER BY created_at DESC, id DESC");
|
||||
push_limit(&mut builder, i64::try_from(query.limit).unwrap_or(i64::MAX));
|
||||
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
node_id,
|
||||
event_type,
|
||||
detail,
|
||||
event_metadata,
|
||||
created_at AS created_at_unix_ms
|
||||
FROM proxy_node_events
|
||||
WHERE node_id = ?
|
||||
AND (? IS NULL OR created_at >= ?)
|
||||
AND (? IS NULL OR created_at <= ?)
|
||||
AND (? IS NULL OR LOWER(event_type) = LOWER(?))
|
||||
ORDER BY created_at DESC, id DESC
|
||||
LIMIT ?
|
||||
"#,
|
||||
)
|
||||
.bind(node_id)
|
||||
.bind(
|
||||
query
|
||||
.from_unix_secs
|
||||
.map(|v| i64::try_from(v).unwrap_or(i64::MAX)),
|
||||
)
|
||||
.bind(
|
||||
query
|
||||
.from_unix_secs
|
||||
.map(|v| i64::try_from(v).unwrap_or(i64::MAX)),
|
||||
)
|
||||
.bind(
|
||||
query
|
||||
.to_unix_secs
|
||||
.map(|v| i64::try_from(v).unwrap_or(i64::MAX)),
|
||||
)
|
||||
.bind(
|
||||
query
|
||||
.to_unix_secs
|
||||
.map(|v| i64::try_from(v).unwrap_or(i64::MAX)),
|
||||
)
|
||||
.bind(query.event_type.as_deref())
|
||||
.bind(query.event_type.as_deref())
|
||||
.bind(i64::try_from(query.limit).unwrap_or(i64::MAX))
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
rows.iter().map(map_proxy_node_event_row).collect()
|
||||
}
|
||||
|
||||
|
||||
@@ -25,26 +25,22 @@ fn quota_snapshot_select() -> SelectQuery<'static> {
|
||||
SelectColumn::expr(DialectSql::dialect(
|
||||
"CAST(monthly_quota_usd AS DOUBLE PRECISION)",
|
||||
"CAST(monthly_quota_usd AS REAL)",
|
||||
"monthly_quota_usd",
|
||||
))
|
||||
.alias("monthly_quota_usd"),
|
||||
SelectColumn::expr(DialectSql::dialect(
|
||||
"CAST(COALESCE(monthly_used_usd, 0) AS DOUBLE PRECISION)",
|
||||
"CAST(COALESCE(monthly_used_usd, 0) AS REAL)",
|
||||
"COALESCE(monthly_used_usd, 0)",
|
||||
))
|
||||
.alias("monthly_used_usd"),
|
||||
SelectColumn::expr("quota_reset_day"),
|
||||
SelectColumn::expr(DialectSql::dialect(
|
||||
"CAST(EXTRACT(EPOCH FROM quota_last_reset_at) AS BIGINT)",
|
||||
"quota_last_reset_at",
|
||||
"quota_last_reset_at",
|
||||
))
|
||||
.alias("quota_last_reset_at_unix_secs"),
|
||||
SelectColumn::expr(DialectSql::dialect(
|
||||
"CAST(EXTRACT(EPOCH FROM quota_expires_at) AS BIGINT)",
|
||||
"quota_expires_at",
|
||||
"quota_expires_at",
|
||||
))
|
||||
.alias("quota_expires_at_unix_secs"),
|
||||
SelectColumn::expr("is_active"),
|
||||
|
||||
@@ -1,14 +1,25 @@
|
||||
use async_trait::async_trait;
|
||||
use sqlx::{mysql::MySqlRow, MySql, Row};
|
||||
use sqlx::{mysql::MySqlRow, MySql, QueryBuilder, Row};
|
||||
|
||||
use super::{
|
||||
quota_snapshot_select, ProviderQuotaReadRepository, ProviderQuotaWriteRepository,
|
||||
StoredProviderQuotaSnapshot,
|
||||
ProviderQuotaReadRepository, ProviderQuotaWriteRepository, StoredProviderQuotaSnapshot,
|
||||
};
|
||||
use crate::driver::mysql::MysqlPool;
|
||||
use crate::error::SqlResultExt;
|
||||
use crate::DataLayerError;
|
||||
use aether_data_query::SqlDialect;
|
||||
|
||||
const QUOTA_COLUMNS: &str = r#"
|
||||
SELECT
|
||||
id AS provider_id,
|
||||
billing_type,
|
||||
monthly_quota_usd,
|
||||
COALESCE(monthly_used_usd, 0) AS monthly_used_usd,
|
||||
quota_reset_day,
|
||||
quota_last_reset_at AS quota_last_reset_at_unix_secs,
|
||||
quota_expires_at AS quota_expires_at_unix_secs,
|
||||
is_active
|
||||
FROM providers
|
||||
"#;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MysqlProviderQuotaRepository {
|
||||
@@ -27,11 +38,8 @@ impl ProviderQuotaReadRepository for MysqlProviderQuotaRepository {
|
||||
&self,
|
||||
provider_id: &str,
|
||||
) -> Result<Option<StoredProviderQuotaSnapshot>, DataLayerError> {
|
||||
let mut statement = quota_snapshot_select().statement::<MySql>(SqlDialect::Mysql);
|
||||
statement.where_eq("id", provider_id.to_string()).limit(1);
|
||||
let row = statement
|
||||
.finish()
|
||||
.build()
|
||||
let row = sqlx::query(&format!("{QUOTA_COLUMNS} WHERE id = ? LIMIT 1"))
|
||||
.bind(provider_id)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
@@ -46,16 +54,16 @@ impl ProviderQuotaReadRepository for MysqlProviderQuotaRepository {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let mut statement = quota_snapshot_select().statement::<MySql>(SqlDialect::Mysql);
|
||||
statement
|
||||
.where_in("id", provider_ids)
|
||||
.order_by_sql("id ASC");
|
||||
let rows = statement
|
||||
.finish()
|
||||
.build()
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_sql_err()?;
|
||||
let mut builder = QueryBuilder::<MySql>::new(QUOTA_COLUMNS);
|
||||
builder.push(" WHERE id IN (");
|
||||
{
|
||||
let mut separated = builder.separated(", ");
|
||||
for provider_id in provider_ids {
|
||||
separated.push_bind(provider_id);
|
||||
}
|
||||
}
|
||||
builder.push(") ORDER BY id ASC");
|
||||
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
||||
rows.iter().map(map_row).collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
This inventory tracks repository read paths that are intended to use the
|
||||
internal `aether-data-query` helpers. The first layer centralizes SQL fragments;
|
||||
the newer `SelectQuery` layer lets repositories describe simple `SELECT`
|
||||
queries once and render dialect-specific projections for Postgres, SQLite, and
|
||||
MySQL.
|
||||
queries once and render dialect-specific projections for Postgres and SQLite.
|
||||
|
||||
## Included In This Pass
|
||||
|
||||
@@ -27,7 +26,7 @@ MySQL.
|
||||
- `find_by_provider_id`
|
||||
- `find_by_provider_ids`
|
||||
- now uses one `SelectQuery` specification for the quota snapshot projection
|
||||
across Postgres, SQLite, and MySQL
|
||||
across Postgres and SQLite
|
||||
- `provider_catalog`
|
||||
- provider by-id/provider list reads in PG/SQLite
|
||||
- endpoint/key by-id and by-provider-id `IN` reads in PG/SQLite
|
||||
@@ -63,9 +62,6 @@ MySQL.
|
||||
- `wallet` ledger, order, refund, callback, and redeem-code list logic.
|
||||
- write/upsert/delete paths, transactions, `RETURNING`, CTEs, window functions,
|
||||
advisory locks, and schema compatibility probes.
|
||||
- MySQL `provider_catalog` still delegates read paths through the existing
|
||||
memory adapter; migrate it in a focused follow-up so MySQL parity can be tested
|
||||
independently.
|
||||
- `users/auth` and `global_models` still contain additional simple read paths.
|
||||
`global_models/sqlite.rs` had pre-existing local edits and must be handled
|
||||
carefully in a dedicated slice.
|
||||
|
||||
Reference in New Issue
Block a user