mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
refactor(data): introduce simple query helper
This commit is contained in:
8
Cargo.lock
generated
8
Cargo.lock
generated
@@ -126,6 +126,7 @@ dependencies = [
|
|||||||
"aether-ai-formats",
|
"aether-ai-formats",
|
||||||
"aether-cache",
|
"aether-cache",
|
||||||
"aether-data-contracts",
|
"aether-data-contracts",
|
||||||
|
"aether-data-query",
|
||||||
"aether-wallet",
|
"aether-wallet",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -155,6 +156,13 @@ dependencies = [
|
|||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aether-data-query"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"sqlx",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aether-data-schema"
|
name = "aether-data-schema"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ members = [
|
|||||||
"crates/aether-pool-core",
|
"crates/aether-pool-core",
|
||||||
"crates/aether-provider-pool",
|
"crates/aether-provider-pool",
|
||||||
"crates/aether-data-contracts",
|
"crates/aether-data-contracts",
|
||||||
|
"crates/aether-data-query",
|
||||||
"crates/aether-data-schema",
|
"crates/aether-data-schema",
|
||||||
"crates/aether-dispatch-core",
|
"crates/aether-dispatch-core",
|
||||||
"crates/aether-cache",
|
"crates/aether-cache",
|
||||||
@@ -42,6 +43,7 @@ aether-ai-serving = { path = "crates/aether-ai-serving" }
|
|||||||
aether-pool-core = { path = "crates/aether-pool-core" }
|
aether-pool-core = { path = "crates/aether-pool-core" }
|
||||||
aether-provider-pool = { path = "crates/aether-provider-pool" }
|
aether-provider-pool = { path = "crates/aether-provider-pool" }
|
||||||
aether-data-contracts = { path = "crates/aether-data-contracts" }
|
aether-data-contracts = { path = "crates/aether-data-contracts" }
|
||||||
|
aether-data-query = { path = "crates/aether-data-query" }
|
||||||
aether-data-schema = { path = "crates/aether-data-schema" }
|
aether-data-schema = { path = "crates/aether-data-schema" }
|
||||||
aether-dispatch-core = { path = "crates/aether-dispatch-core" }
|
aether-dispatch-core = { path = "crates/aether-dispatch-core" }
|
||||||
aether-cache = { path = "crates/aether-cache" }
|
aether-cache = { path = "crates/aether-cache" }
|
||||||
|
|||||||
10
crates/aether-data-query/Cargo.toml
Normal file
10
crates/aether-data-query/Cargo.toml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[package]
|
||||||
|
name = "aether-data-query"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
description = "Small SQL dialect helpers for Aether data repositories"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
sqlx.workspace = true
|
||||||
363
crates/aether-data-query/src/lib.rs
Normal file
363
crates/aether-data-query/src/lib.rs
Normal file
@@ -0,0 +1,363 @@
|
|||||||
|
use sqlx::{Database, Encode, QueryBuilder, Type};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
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}")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn quote_path(self, parts: &[&str]) -> String {
|
||||||
|
parts
|
||||||
|
.iter()
|
||||||
|
.map(|part| self.quote_ident(part))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(".")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||||
|
pub struct WhereClause {
|
||||||
|
has_clause: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WhereClause {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self { has_clause: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_existing_clause() -> Self {
|
||||||
|
Self { has_clause: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_empty(self) -> bool {
|
||||||
|
!self.has_clause
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push_next<DB>(&mut self, builder: &mut QueryBuilder<'_, DB>)
|
||||||
|
where
|
||||||
|
DB: Database,
|
||||||
|
{
|
||||||
|
if self.has_clause {
|
||||||
|
builder.push(" AND ");
|
||||||
|
} else {
|
||||||
|
builder.push(" WHERE ");
|
||||||
|
self.has_clause = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum SortDirection {
|
||||||
|
Asc,
|
||||||
|
Desc,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SortDirection {
|
||||||
|
pub fn sql(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Asc => "ASC",
|
||||||
|
Self::Desc => "DESC",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct OrderByColumn<'a> {
|
||||||
|
pub key: &'a str,
|
||||||
|
pub sql: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push_eq<'args, DB, T>(
|
||||||
|
builder: &mut QueryBuilder<'args, DB>,
|
||||||
|
where_clause: &mut WhereClause,
|
||||||
|
column_sql: &str,
|
||||||
|
value: T,
|
||||||
|
) where
|
||||||
|
DB: Database,
|
||||||
|
T: 'args + Encode<'args, DB> + Type<DB>,
|
||||||
|
{
|
||||||
|
where_clause.push_next(builder);
|
||||||
|
builder.push(column_sql).push(" = ").push_bind(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push_optional_eq<'args, DB, T>(
|
||||||
|
builder: &mut QueryBuilder<'args, DB>,
|
||||||
|
where_clause: &mut WhereClause,
|
||||||
|
column_sql: &str,
|
||||||
|
value: Option<T>,
|
||||||
|
) where
|
||||||
|
DB: Database,
|
||||||
|
T: 'args + Encode<'args, DB> + Type<DB>,
|
||||||
|
{
|
||||||
|
if let Some(value) = value {
|
||||||
|
push_eq(builder, where_clause, column_sql, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push_in<'args, DB, T>(
|
||||||
|
builder: &mut QueryBuilder<'args, DB>,
|
||||||
|
where_clause: &mut WhereClause,
|
||||||
|
column_sql: &str,
|
||||||
|
values: &[T],
|
||||||
|
) where
|
||||||
|
DB: Database,
|
||||||
|
T: Clone + 'args + Encode<'args, DB> + Type<DB>,
|
||||||
|
{
|
||||||
|
where_clause.push_next(builder);
|
||||||
|
builder.push(column_sql).push(" IN (");
|
||||||
|
{
|
||||||
|
let mut separated = builder.separated(", ");
|
||||||
|
for value in values {
|
||||||
|
separated.push_bind(value.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
builder.push(")");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push_ci_contains<'args, DB>(
|
||||||
|
builder: &mut QueryBuilder<'args, DB>,
|
||||||
|
where_clause: &mut WhereClause,
|
||||||
|
dialect: SqlDialect,
|
||||||
|
column_sql: &str,
|
||||||
|
value: &str,
|
||||||
|
) where
|
||||||
|
DB: Database,
|
||||||
|
String: 'args + Encode<'args, DB> + Type<DB>,
|
||||||
|
{
|
||||||
|
let trimmed = value.trim();
|
||||||
|
if trimmed.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
where_clause.push_next(builder);
|
||||||
|
push_ci_contains_predicate(builder, dialect, column_sql, trimmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push_ci_contains_any<'args, DB>(
|
||||||
|
builder: &mut QueryBuilder<'args, DB>,
|
||||||
|
where_clause: &mut WhereClause,
|
||||||
|
dialect: SqlDialect,
|
||||||
|
column_sqls: &[&str],
|
||||||
|
value: &str,
|
||||||
|
) where
|
||||||
|
DB: Database,
|
||||||
|
String: 'args + Encode<'args, DB> + Type<DB>,
|
||||||
|
{
|
||||||
|
let trimmed = value.trim();
|
||||||
|
if trimmed.is_empty() || column_sqls.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
where_clause.push_next(builder);
|
||||||
|
builder.push("(");
|
||||||
|
for (index, column_sql) in column_sqls.iter().enumerate() {
|
||||||
|
if index > 0 {
|
||||||
|
builder.push(" OR ");
|
||||||
|
}
|
||||||
|
push_ci_contains_predicate(builder, dialect, column_sql, trimmed);
|
||||||
|
}
|
||||||
|
builder.push(")");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_ci_contains_predicate<'args, DB>(
|
||||||
|
builder: &mut QueryBuilder<'args, DB>,
|
||||||
|
dialect: SqlDialect,
|
||||||
|
column_sql: &str,
|
||||||
|
trimmed: &str,
|
||||||
|
) where
|
||||||
|
DB: Database,
|
||||||
|
String: 'args + Encode<'args, DB> + Type<DB>,
|
||||||
|
{
|
||||||
|
match dialect {
|
||||||
|
SqlDialect::Postgres => {
|
||||||
|
builder
|
||||||
|
.push(column_sql)
|
||||||
|
.push(" ILIKE ")
|
||||||
|
.push_bind(format!("%{trimmed}%"));
|
||||||
|
}
|
||||||
|
SqlDialect::Sqlite | SqlDialect::Mysql => {
|
||||||
|
builder
|
||||||
|
.push("LOWER(")
|
||||||
|
.push(column_sql)
|
||||||
|
.push(") LIKE ")
|
||||||
|
.push_bind(format!("%{}%", trimmed.to_ascii_lowercase()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push_limit<'args, DB>(builder: &mut QueryBuilder<'args, DB>, limit: i64)
|
||||||
|
where
|
||||||
|
DB: Database,
|
||||||
|
i64: 'args + Encode<'args, DB> + Type<DB>,
|
||||||
|
{
|
||||||
|
builder.push(" LIMIT ").push_bind(limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push_limit_offset<'args, DB>(builder: &mut QueryBuilder<'args, DB>, limit: i64, offset: i64)
|
||||||
|
where
|
||||||
|
DB: Database,
|
||||||
|
i64: 'args + Encode<'args, DB> + Type<DB>,
|
||||||
|
{
|
||||||
|
push_limit(builder, limit);
|
||||||
|
builder.push(" OFFSET ").push_bind(offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push_order_by<DB>(
|
||||||
|
builder: &mut QueryBuilder<'_, DB>,
|
||||||
|
requested_key: Option<&str>,
|
||||||
|
direction: SortDirection,
|
||||||
|
allowed: &[OrderByColumn<'_>],
|
||||||
|
default_key: &str,
|
||||||
|
) where
|
||||||
|
DB: Database,
|
||||||
|
{
|
||||||
|
let key = requested_key.unwrap_or(default_key);
|
||||||
|
let column = allowed
|
||||||
|
.iter()
|
||||||
|
.find(|column| column.key == key)
|
||||||
|
.or_else(|| allowed.iter().find(|column| column.key == default_key))
|
||||||
|
.expect("default order column must be allowed");
|
||||||
|
builder
|
||||||
|
.push(" ORDER BY ")
|
||||||
|
.push(column.sql)
|
||||||
|
.push(" ")
|
||||||
|
.push(direction.sql());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use sqlx::{Execute, MySql, 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\""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn where_clause_pushes_where_then_and() {
|
||||||
|
let mut builder = QueryBuilder::<Sqlite>::new("SELECT * FROM items");
|
||||||
|
let mut where_clause = WhereClause::new();
|
||||||
|
push_eq(
|
||||||
|
&mut builder,
|
||||||
|
&mut where_clause,
|
||||||
|
"kind",
|
||||||
|
"scheduled".to_string(),
|
||||||
|
);
|
||||||
|
push_eq(
|
||||||
|
&mut builder,
|
||||||
|
&mut where_clause,
|
||||||
|
"status",
|
||||||
|
"running".to_string(),
|
||||||
|
);
|
||||||
|
let query = builder.build();
|
||||||
|
assert!(query.sql().contains(" WHERE kind = ? AND status = ?"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ci_contains_uses_ilike_for_postgres() {
|
||||||
|
let mut builder = QueryBuilder::<Postgres>::new("SELECT * FROM items");
|
||||||
|
let mut where_clause = WhereClause::new();
|
||||||
|
push_ci_contains(
|
||||||
|
&mut builder,
|
||||||
|
&mut where_clause,
|
||||||
|
SqlDialect::Postgres,
|
||||||
|
"task_key",
|
||||||
|
" Fetch ",
|
||||||
|
);
|
||||||
|
let query = builder.build();
|
||||||
|
assert!(query.sql().contains(" WHERE task_key ILIKE $1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ci_contains_uses_lower_like_for_sqlite_and_mysql() {
|
||||||
|
let mut sqlite_builder = QueryBuilder::<Sqlite>::new("SELECT * FROM items");
|
||||||
|
let mut sqlite_where = WhereClause::new();
|
||||||
|
push_ci_contains(
|
||||||
|
&mut sqlite_builder,
|
||||||
|
&mut sqlite_where,
|
||||||
|
SqlDialect::Sqlite,
|
||||||
|
"task_key",
|
||||||
|
" Fetch ",
|
||||||
|
);
|
||||||
|
assert!(sqlite_builder
|
||||||
|
.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]
|
||||||
|
fn ci_contains_any_groups_or_predicates() {
|
||||||
|
let mut builder = QueryBuilder::<Sqlite>::new("SELECT * FROM items");
|
||||||
|
let mut where_clause = WhereClause::new();
|
||||||
|
push_ci_contains_any(
|
||||||
|
&mut builder,
|
||||||
|
&mut where_clause,
|
||||||
|
SqlDialect::Sqlite,
|
||||||
|
&["file_name", "COALESCE(display_name, '')"],
|
||||||
|
"Avatar",
|
||||||
|
);
|
||||||
|
let query = builder.build();
|
||||||
|
assert!(query.sql().contains(
|
||||||
|
" WHERE (LOWER(file_name) LIKE ? OR LOWER(COALESCE(display_name, '')) LIKE ?)"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn in_limit_offset_and_order_are_rendered() {
|
||||||
|
let mut builder = QueryBuilder::<Sqlite>::new("SELECT * FROM items");
|
||||||
|
let mut where_clause = WhereClause::new();
|
||||||
|
push_in(
|
||||||
|
&mut builder,
|
||||||
|
&mut where_clause,
|
||||||
|
"id",
|
||||||
|
&["a".to_string(), "b".to_string()],
|
||||||
|
);
|
||||||
|
push_order_by(
|
||||||
|
&mut builder,
|
||||||
|
Some("created"),
|
||||||
|
SortDirection::Desc,
|
||||||
|
&[OrderByColumn {
|
||||||
|
key: "created",
|
||||||
|
sql: "created_at",
|
||||||
|
}],
|
||||||
|
"created",
|
||||||
|
);
|
||||||
|
push_limit_offset(&mut builder, 10, 5);
|
||||||
|
let query = builder.build();
|
||||||
|
assert!(query.sql().contains(" WHERE id IN (?, ?)"));
|
||||||
|
assert!(query.sql().contains(" ORDER BY created_at DESC"));
|
||||||
|
assert!(query.sql().contains(" LIMIT ? OFFSET ?"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ build = "build.rs"
|
|||||||
aether-ai-formats.workspace = true
|
aether-ai-formats.workspace = true
|
||||||
aether-data-contracts.workspace = true
|
aether-data-contracts.workspace = true
|
||||||
aether-cache.workspace = true
|
aether-cache.workspace = true
|
||||||
|
aether-data-query.workspace = true
|
||||||
aether-wallet.workspace = true
|
aether-wallet.workspace = true
|
||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use sqlx::{mysql::MySqlRow, Row};
|
use sqlx::{mysql::MySqlRow, MySql, QueryBuilder, Row};
|
||||||
|
|
||||||
use super::types::{
|
use super::types::{
|
||||||
AnnouncementListQuery, AnnouncementReadRepository, AnnouncementWriteRepository,
|
AnnouncementListQuery, AnnouncementReadRepository, AnnouncementWriteRepository,
|
||||||
@@ -8,6 +8,7 @@ use super::types::{
|
|||||||
use crate::driver::mysql::MysqlPool;
|
use crate::driver::mysql::MysqlPool;
|
||||||
use crate::error::SqlResultExt;
|
use crate::error::SqlResultExt;
|
||||||
use crate::DataLayerError;
|
use crate::DataLayerError;
|
||||||
|
use aether_data_query::{push_eq, push_limit, push_limit_offset, WhereClause};
|
||||||
|
|
||||||
const ANNOUNCEMENT_SELECT: &str = r#"
|
const ANNOUNCEMENT_SELECT: &str = r#"
|
||||||
SELECT
|
SELECT
|
||||||
@@ -44,6 +45,27 @@ impl MysqlAnnouncementRepository {
|
|||||||
) -> Result<Option<StoredAnnouncement>, DataLayerError> {
|
) -> Result<Option<StoredAnnouncement>, DataLayerError> {
|
||||||
self.find_by_id(announcement_id).await
|
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]
|
#[async_trait]
|
||||||
@@ -52,8 +74,17 @@ impl AnnouncementReadRepository for MysqlAnnouncementRepository {
|
|||||||
&self,
|
&self,
|
||||||
announcement_id: &str,
|
announcement_id: &str,
|
||||||
) -> Result<Option<StoredAnnouncement>, DataLayerError> {
|
) -> Result<Option<StoredAnnouncement>, DataLayerError> {
|
||||||
let row = sqlx::query(&format!("{ANNOUNCEMENT_SELECT} WHERE a.id = ? LIMIT 1"))
|
let mut builder = QueryBuilder::<MySql>::new(ANNOUNCEMENT_SELECT);
|
||||||
.bind(announcement_id)
|
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()
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_sql_err()?;
|
.map_sql_err()?;
|
||||||
@@ -65,46 +96,35 @@ impl AnnouncementReadRepository for MysqlAnnouncementRepository {
|
|||||||
query: &AnnouncementListQuery,
|
query: &AnnouncementListQuery,
|
||||||
) -> Result<StoredAnnouncementPage, DataLayerError> {
|
) -> Result<StoredAnnouncementPage, DataLayerError> {
|
||||||
let now_unix_secs = query.now_unix_secs.unwrap_or_else(current_unix_secs);
|
let now_unix_secs = query.now_unix_secs.unwrap_or_else(current_unix_secs);
|
||||||
let total_row = sqlx::query(
|
let mut count_builder =
|
||||||
r#"
|
QueryBuilder::<MySql>::new("SELECT COUNT(a.id) AS total FROM announcements a");
|
||||||
SELECT COUNT(a.id) AS total
|
let mut count_where = WhereClause::new();
|
||||||
FROM announcements a
|
Self::apply_active_filter(
|
||||||
WHERE (
|
&mut count_builder,
|
||||||
NOT ? OR (
|
&mut count_where,
|
||||||
a.is_active = 1
|
query.active_only,
|
||||||
AND (a.start_time IS NULL OR a.start_time <= ?)
|
now_unix_secs,
|
||||||
AND (a.end_time IS NULL OR a.end_time >= ?)
|
)?;
|
||||||
)
|
let total = count_builder
|
||||||
)
|
.build_query_scalar::<i64>()
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.bind(query.active_only)
|
|
||||||
.bind(now_unix_secs as i64)
|
|
||||||
.bind(now_unix_secs as i64)
|
|
||||||
.fetch_one(&self.pool)
|
.fetch_one(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_sql_err()?;
|
.map_sql_err()?
|
||||||
let total = total_row.try_get::<i64, _>("total").map_sql_err()?.max(0) as u64;
|
.max(0) as u64;
|
||||||
|
|
||||||
let rows = sqlx::query(&format!(
|
let mut list_builder = QueryBuilder::<MySql>::new(ANNOUNCEMENT_SELECT);
|
||||||
r#"
|
let mut list_where = WhereClause::new();
|
||||||
{ANNOUNCEMENT_SELECT}
|
Self::apply_active_filter(
|
||||||
WHERE (
|
&mut list_builder,
|
||||||
NOT ? OR (
|
&mut list_where,
|
||||||
a.is_active = 1
|
query.active_only,
|
||||||
AND (a.start_time IS NULL OR a.start_time <= ?)
|
now_unix_secs,
|
||||||
AND (a.end_time IS NULL OR a.end_time >= ?)
|
)?;
|
||||||
)
|
list_builder
|
||||||
)
|
.push(" ORDER BY a.is_pinned DESC, a.priority DESC, a.created_at DESC, a.id ASC");
|
||||||
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);
|
||||||
LIMIT ? OFFSET ?
|
let rows = list_builder
|
||||||
"#
|
.build()
|
||||||
))
|
|
||||||
.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)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_sql_err()?;
|
.map_sql_err()?;
|
||||||
@@ -121,28 +141,22 @@ LIMIT ? OFFSET ?
|
|||||||
user_id: &str,
|
user_id: &str,
|
||||||
now_unix_secs: u64,
|
now_unix_secs: u64,
|
||||||
) -> Result<u64, DataLayerError> {
|
) -> Result<u64, DataLayerError> {
|
||||||
let row = sqlx::query(
|
let mut builder =
|
||||||
r#"
|
QueryBuilder::<MySql>::new("SELECT COUNT(a.id) AS total FROM announcements a");
|
||||||
SELECT COUNT(a.id) AS total
|
let mut where_clause = WhereClause::new();
|
||||||
FROM announcements a
|
Self::apply_active_filter(&mut builder, &mut where_clause, true, now_unix_secs)?;
|
||||||
WHERE a.is_active = 1
|
where_clause.push_next(&mut builder);
|
||||||
AND (a.start_time IS NULL OR a.start_time <= ?)
|
builder
|
||||||
AND (a.end_time IS NULL OR a.end_time >= ?)
|
.push("NOT EXISTS (SELECT 1 FROM announcement_reads r WHERE r.user_id = ")
|
||||||
AND NOT EXISTS (
|
.push_bind(user_id.to_string())
|
||||||
SELECT 1
|
.push(" AND r.announcement_id = a.id)");
|
||||||
FROM announcement_reads r
|
let total = builder
|
||||||
WHERE r.user_id = ?
|
.build_query_scalar::<i64>()
|
||||||
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)
|
.fetch_one(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_sql_err()?;
|
.map_sql_err()?
|
||||||
Ok(row.try_get::<i64, _>("total").map_sql_err()?.max(0) as u64)
|
.max(0) as u64;
|
||||||
|
Ok(total)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use chrono::{TimeZone, Utc};
|
use chrono::{TimeZone, Utc};
|
||||||
use futures_util::TryStreamExt;
|
use sqlx::{postgres::PgRow, PgPool, Postgres, QueryBuilder, Row};
|
||||||
use sqlx::{postgres::PgRow, PgPool, Row};
|
|
||||||
|
|
||||||
use super::types::{
|
use super::types::{
|
||||||
AnnouncementListQuery, AnnouncementReadRepository, AnnouncementWriteRepository,
|
AnnouncementListQuery, AnnouncementReadRepository, AnnouncementWriteRepository,
|
||||||
CreateAnnouncementRecord, StoredAnnouncement, StoredAnnouncementPage, UpdateAnnouncementRecord,
|
CreateAnnouncementRecord, StoredAnnouncement, StoredAnnouncementPage, UpdateAnnouncementRecord,
|
||||||
};
|
};
|
||||||
use crate::{error::SqlxResultExt, DataLayerError};
|
use crate::{error::SqlxResultExt, DataLayerError};
|
||||||
|
use aether_data_query::{push_eq, push_limit, push_limit_offset, WhereClause};
|
||||||
|
|
||||||
const FIND_ANNOUNCEMENT_BY_ID_SQL: &str = r#"
|
const ANNOUNCEMENT_SELECT: &str = r#"
|
||||||
SELECT
|
SELECT
|
||||||
a.id,
|
a.id,
|
||||||
a.title,
|
a.title,
|
||||||
@@ -26,63 +26,6 @@ SELECT
|
|||||||
EXTRACT(EPOCH FROM a.updated_at)::bigint AS updated_at_unix_secs
|
EXTRACT(EPOCH FROM a.updated_at)::bigint AS updated_at_unix_secs
|
||||||
FROM announcements a
|
FROM announcements a
|
||||||
LEFT JOIN users u ON u.id = a.author_id
|
LEFT JOIN users u ON u.id = a.author_id
|
||||||
WHERE a.id = $1
|
|
||||||
LIMIT 1
|
|
||||||
"#;
|
|
||||||
|
|
||||||
const LIST_ANNOUNCEMENTS_SQL: &str = r#"
|
|
||||||
SELECT
|
|
||||||
a.id,
|
|
||||||
a.title,
|
|
||||||
a.content,
|
|
||||||
a.type,
|
|
||||||
a.priority,
|
|
||||||
a.is_active,
|
|
||||||
a.is_pinned,
|
|
||||||
a.author_id,
|
|
||||||
u.username AS author_username,
|
|
||||||
EXTRACT(EPOCH FROM a.start_time)::bigint AS start_time_unix_secs,
|
|
||||||
EXTRACT(EPOCH FROM a.end_time)::bigint AS end_time_unix_secs,
|
|
||||||
EXTRACT(EPOCH FROM a.created_at)::bigint AS created_at_unix_ms,
|
|
||||||
EXTRACT(EPOCH FROM a.updated_at)::bigint AS updated_at_unix_secs
|
|
||||||
FROM announcements a
|
|
||||||
LEFT JOIN users u ON u.id = a.author_id
|
|
||||||
WHERE (
|
|
||||||
NOT $1 OR (
|
|
||||||
a.is_active = TRUE
|
|
||||||
AND (a.start_time IS NULL OR a.start_time <= TO_TIMESTAMP($2::double precision))
|
|
||||||
AND (a.end_time IS NULL OR a.end_time >= TO_TIMESTAMP($2::double precision))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
ORDER BY a.is_pinned DESC, a.priority DESC, a.created_at DESC, a.id ASC
|
|
||||||
OFFSET $3
|
|
||||||
LIMIT $4
|
|
||||||
"#;
|
|
||||||
|
|
||||||
const COUNT_ANNOUNCEMENTS_SQL: &str = r#"
|
|
||||||
SELECT COUNT(a.id) AS total
|
|
||||||
FROM announcements a
|
|
||||||
WHERE (
|
|
||||||
NOT $1 OR (
|
|
||||||
a.is_active = TRUE
|
|
||||||
AND (a.start_time IS NULL OR a.start_time <= TO_TIMESTAMP($2::double precision))
|
|
||||||
AND (a.end_time IS NULL OR a.end_time >= TO_TIMESTAMP($2::double precision))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
"#;
|
|
||||||
|
|
||||||
const COUNT_UNREAD_ACTIVE_ANNOUNCEMENTS_SQL: &str = r#"
|
|
||||||
SELECT COUNT(a.id) AS total
|
|
||||||
FROM announcements a
|
|
||||||
WHERE a.is_active = TRUE
|
|
||||||
AND (a.start_time IS NULL OR a.start_time <= TO_TIMESTAMP($2::double precision))
|
|
||||||
AND (a.end_time IS NULL OR a.end_time >= TO_TIMESTAMP($2::double precision))
|
|
||||||
AND NOT EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM announcement_reads r
|
|
||||||
WHERE r.user_id = $1
|
|
||||||
AND r.announcement_id = a.id
|
|
||||||
)
|
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
const CREATE_ANNOUNCEMENT_SQL: &str = r#"
|
const CREATE_ANNOUNCEMENT_SQL: &str = r#"
|
||||||
@@ -193,6 +136,25 @@ impl SqlxAnnouncementReadRepository {
|
|||||||
pub fn new(pool: PgPool) -> Self {
|
pub fn new(pool: PgPool) -> Self {
|
||||||
Self { pool }
|
Self { pool }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn apply_active_filter(
|
||||||
|
builder: &mut QueryBuilder<'_, Postgres>,
|
||||||
|
where_clause: &mut WhereClause,
|
||||||
|
active_only: bool,
|
||||||
|
now_unix_secs: u64,
|
||||||
|
) {
|
||||||
|
if !active_only {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
where_clause.push_next(builder);
|
||||||
|
builder
|
||||||
|
.push("a.is_active = TRUE AND (a.start_time IS NULL OR a.start_time <= TO_TIMESTAMP(")
|
||||||
|
.push_bind(now_unix_secs as f64)
|
||||||
|
.push("::double precision)) AND (a.end_time IS NULL OR a.end_time >= TO_TIMESTAMP(")
|
||||||
|
.push_bind(now_unix_secs as f64)
|
||||||
|
.push("::double precision))");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -201,8 +163,17 @@ impl AnnouncementReadRepository for SqlxAnnouncementReadRepository {
|
|||||||
&self,
|
&self,
|
||||||
announcement_id: &str,
|
announcement_id: &str,
|
||||||
) -> Result<Option<StoredAnnouncement>, DataLayerError> {
|
) -> Result<Option<StoredAnnouncement>, DataLayerError> {
|
||||||
let row = sqlx::query(FIND_ANNOUNCEMENT_BY_ID_SQL)
|
let mut builder = QueryBuilder::<Postgres>::new(ANNOUNCEMENT_SELECT);
|
||||||
.bind(announcement_id)
|
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()
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_postgres_err()?;
|
.map_postgres_err()?;
|
||||||
@@ -214,27 +185,42 @@ impl AnnouncementReadRepository for SqlxAnnouncementReadRepository {
|
|||||||
query: &AnnouncementListQuery,
|
query: &AnnouncementListQuery,
|
||||||
) -> Result<StoredAnnouncementPage, DataLayerError> {
|
) -> Result<StoredAnnouncementPage, DataLayerError> {
|
||||||
let now_unix_secs = query.now_unix_secs.unwrap_or_else(current_unix_secs);
|
let now_unix_secs = query.now_unix_secs.unwrap_or_else(current_unix_secs);
|
||||||
let total_row = sqlx::query(COUNT_ANNOUNCEMENTS_SQL)
|
let mut count_builder =
|
||||||
.bind(query.active_only)
|
QueryBuilder::<Postgres>::new("SELECT COUNT(a.id) AS total FROM announcements a");
|
||||||
.bind(now_unix_secs as f64)
|
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)
|
.fetch_one(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_postgres_err()?;
|
|
||||||
let total = total_row
|
|
||||||
.try_get::<i64, _>("total")
|
|
||||||
.map_postgres_err()?
|
.map_postgres_err()?
|
||||||
.max(0) as u64;
|
.max(0) as u64;
|
||||||
|
|
||||||
let mut rows = sqlx::query(LIST_ANNOUNCEMENTS_SQL)
|
let mut list_builder = QueryBuilder::<Postgres>::new(ANNOUNCEMENT_SELECT);
|
||||||
.bind(query.active_only)
|
let mut list_where = WhereClause::new();
|
||||||
.bind(now_unix_secs as f64)
|
Self::apply_active_filter(
|
||||||
.bind(query.offset as i64)
|
&mut list_builder,
|
||||||
.bind(query.limit as i64)
|
&mut list_where,
|
||||||
.fetch(&self.pool);
|
query.active_only,
|
||||||
let mut items = Vec::new();
|
now_unix_secs,
|
||||||
while let Some(row) = rows.try_next().await.map_postgres_err()? {
|
);
|
||||||
items.push(map_announcement_row(&row)?);
|
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_postgres_err()?;
|
||||||
|
let items = rows
|
||||||
|
.iter()
|
||||||
|
.map(map_announcement_row)
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
|
||||||
Ok(StoredAnnouncementPage { items, total })
|
Ok(StoredAnnouncementPage { items, total })
|
||||||
}
|
}
|
||||||
@@ -244,13 +230,22 @@ impl AnnouncementReadRepository for SqlxAnnouncementReadRepository {
|
|||||||
user_id: &str,
|
user_id: &str,
|
||||||
now_unix_secs: u64,
|
now_unix_secs: u64,
|
||||||
) -> Result<u64, DataLayerError> {
|
) -> Result<u64, DataLayerError> {
|
||||||
let row = sqlx::query(COUNT_UNREAD_ACTIVE_ANNOUNCEMENTS_SQL)
|
let mut builder =
|
||||||
.bind(user_id)
|
QueryBuilder::<Postgres>::new("SELECT COUNT(a.id) AS total FROM announcements a");
|
||||||
.bind(now_unix_secs as f64)
|
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)
|
.fetch_one(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_postgres_err()?;
|
.map_postgres_err()?
|
||||||
Ok(row.try_get::<i64, _>("total").map_postgres_err()?.max(0) as u64)
|
.max(0) as u64;
|
||||||
|
Ok(total)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use sqlx::{sqlite::SqliteRow, Row};
|
use sqlx::{sqlite::SqliteRow, QueryBuilder, Row, Sqlite};
|
||||||
|
|
||||||
use super::types::{
|
use super::types::{
|
||||||
AnnouncementListQuery, AnnouncementReadRepository, AnnouncementWriteRepository,
|
AnnouncementListQuery, AnnouncementReadRepository, AnnouncementWriteRepository,
|
||||||
@@ -8,6 +8,7 @@ use super::types::{
|
|||||||
use crate::driver::sqlite::SqlitePool;
|
use crate::driver::sqlite::SqlitePool;
|
||||||
use crate::error::SqlResultExt;
|
use crate::error::SqlResultExt;
|
||||||
use crate::DataLayerError;
|
use crate::DataLayerError;
|
||||||
|
use aether_data_query::{push_eq, push_limit, push_limit_offset, WhereClause};
|
||||||
|
|
||||||
const ANNOUNCEMENT_SELECT: &str = r#"
|
const ANNOUNCEMENT_SELECT: &str = r#"
|
||||||
SELECT
|
SELECT
|
||||||
@@ -44,6 +45,27 @@ impl SqliteAnnouncementRepository {
|
|||||||
) -> Result<Option<StoredAnnouncement>, DataLayerError> {
|
) -> Result<Option<StoredAnnouncement>, DataLayerError> {
|
||||||
self.find_by_id(announcement_id).await
|
self.find_by_id(announcement_id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn apply_active_filter(
|
||||||
|
builder: &mut QueryBuilder<'_, Sqlite>,
|
||||||
|
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]
|
#[async_trait]
|
||||||
@@ -52,8 +74,17 @@ impl AnnouncementReadRepository for SqliteAnnouncementRepository {
|
|||||||
&self,
|
&self,
|
||||||
announcement_id: &str,
|
announcement_id: &str,
|
||||||
) -> Result<Option<StoredAnnouncement>, DataLayerError> {
|
) -> Result<Option<StoredAnnouncement>, DataLayerError> {
|
||||||
let row = sqlx::query(&format!("{ANNOUNCEMENT_SELECT} WHERE a.id = ? LIMIT 1"))
|
let mut builder = QueryBuilder::<Sqlite>::new(ANNOUNCEMENT_SELECT);
|
||||||
.bind(announcement_id)
|
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()
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_sql_err()?;
|
.map_sql_err()?;
|
||||||
@@ -65,46 +96,35 @@ impl AnnouncementReadRepository for SqliteAnnouncementRepository {
|
|||||||
query: &AnnouncementListQuery,
|
query: &AnnouncementListQuery,
|
||||||
) -> Result<StoredAnnouncementPage, DataLayerError> {
|
) -> Result<StoredAnnouncementPage, DataLayerError> {
|
||||||
let now_unix_secs = query.now_unix_secs.unwrap_or_else(current_unix_secs);
|
let now_unix_secs = query.now_unix_secs.unwrap_or_else(current_unix_secs);
|
||||||
let total_row = sqlx::query(
|
let mut count_builder =
|
||||||
r#"
|
QueryBuilder::<Sqlite>::new("SELECT COUNT(a.id) AS total FROM announcements a");
|
||||||
SELECT COUNT(a.id) AS total
|
let mut count_where = WhereClause::new();
|
||||||
FROM announcements a
|
Self::apply_active_filter(
|
||||||
WHERE (
|
&mut count_builder,
|
||||||
NOT ? OR (
|
&mut count_where,
|
||||||
a.is_active = 1
|
query.active_only,
|
||||||
AND (a.start_time IS NULL OR a.start_time <= ?)
|
now_unix_secs,
|
||||||
AND (a.end_time IS NULL OR a.end_time >= ?)
|
)?;
|
||||||
)
|
let total = count_builder
|
||||||
)
|
.build_query_scalar::<i64>()
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.bind(query.active_only)
|
|
||||||
.bind(now_unix_secs as i64)
|
|
||||||
.bind(now_unix_secs as i64)
|
|
||||||
.fetch_one(&self.pool)
|
.fetch_one(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_sql_err()?;
|
.map_sql_err()?
|
||||||
let total = total_row.try_get::<i64, _>("total").map_sql_err()?.max(0) as u64;
|
.max(0) as u64;
|
||||||
|
|
||||||
let rows = sqlx::query(&format!(
|
let mut list_builder = QueryBuilder::<Sqlite>::new(ANNOUNCEMENT_SELECT);
|
||||||
r#"
|
let mut list_where = WhereClause::new();
|
||||||
{ANNOUNCEMENT_SELECT}
|
Self::apply_active_filter(
|
||||||
WHERE (
|
&mut list_builder,
|
||||||
NOT ? OR (
|
&mut list_where,
|
||||||
a.is_active = 1
|
query.active_only,
|
||||||
AND (a.start_time IS NULL OR a.start_time <= ?)
|
now_unix_secs,
|
||||||
AND (a.end_time IS NULL OR a.end_time >= ?)
|
)?;
|
||||||
)
|
list_builder
|
||||||
)
|
.push(" ORDER BY a.is_pinned DESC, a.priority DESC, a.created_at DESC, a.id ASC");
|
||||||
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);
|
||||||
LIMIT ? OFFSET ?
|
let rows = list_builder
|
||||||
"#
|
.build()
|
||||||
))
|
|
||||||
.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)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_sql_err()?;
|
.map_sql_err()?;
|
||||||
@@ -121,28 +141,22 @@ LIMIT ? OFFSET ?
|
|||||||
user_id: &str,
|
user_id: &str,
|
||||||
now_unix_secs: u64,
|
now_unix_secs: u64,
|
||||||
) -> Result<u64, DataLayerError> {
|
) -> Result<u64, DataLayerError> {
|
||||||
let row = sqlx::query(
|
let mut builder =
|
||||||
r#"
|
QueryBuilder::<Sqlite>::new("SELECT COUNT(a.id) AS total FROM announcements a");
|
||||||
SELECT COUNT(a.id) AS total
|
let mut where_clause = WhereClause::new();
|
||||||
FROM announcements a
|
Self::apply_active_filter(&mut builder, &mut where_clause, true, now_unix_secs)?;
|
||||||
WHERE a.is_active = 1
|
where_clause.push_next(&mut builder);
|
||||||
AND (a.start_time IS NULL OR a.start_time <= ?)
|
builder
|
||||||
AND (a.end_time IS NULL OR a.end_time >= ?)
|
.push("NOT EXISTS (SELECT 1 FROM announcement_reads r WHERE r.user_id = ")
|
||||||
AND NOT EXISTS (
|
.push_bind(user_id.to_string())
|
||||||
SELECT 1
|
.push(" AND r.announcement_id = a.id)");
|
||||||
FROM announcement_reads r
|
let total = builder
|
||||||
WHERE r.user_id = ?
|
.build_query_scalar::<i64>()
|
||||||
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)
|
.fetch_one(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_sql_err()?;
|
.map_sql_err()?
|
||||||
Ok(row.try_get::<i64, _>("total").map_sql_err()?.max(0) as u64)
|
.max(0) as u64;
|
||||||
|
Ok(total)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use sqlx::{mysql::MySqlRow, Row};
|
use sqlx::{mysql::MySqlRow, MySql, QueryBuilder, Row};
|
||||||
|
|
||||||
use super::types::{
|
use super::types::{
|
||||||
AuthModuleReadRepository, AuthModuleWriteRepository, StoredLdapModuleConfig,
|
AuthModuleReadRepository, AuthModuleWriteRepository, StoredLdapModuleConfig,
|
||||||
@@ -8,8 +8,9 @@ use super::types::{
|
|||||||
use crate::driver::mysql::MysqlPool;
|
use crate::driver::mysql::MysqlPool;
|
||||||
use crate::error::SqlResultExt;
|
use crate::error::SqlResultExt;
|
||||||
use crate::DataLayerError;
|
use crate::DataLayerError;
|
||||||
|
use aether_data_query::{push_eq, push_limit, WhereClause};
|
||||||
|
|
||||||
const LIST_ENABLED_OAUTH_PROVIDERS_SQL: &str = r#"
|
const OAUTH_PROVIDER_COLUMNS: &str = r#"
|
||||||
SELECT
|
SELECT
|
||||||
provider_type,
|
provider_type,
|
||||||
display_name,
|
display_name,
|
||||||
@@ -17,11 +18,9 @@ SELECT
|
|||||||
client_secret_encrypted,
|
client_secret_encrypted,
|
||||||
redirect_uri
|
redirect_uri
|
||||||
FROM oauth_providers
|
FROM oauth_providers
|
||||||
WHERE is_enabled = 1
|
|
||||||
ORDER BY provider_type ASC
|
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
const GET_LDAP_CONFIG_SQL: &str = r#"
|
const LDAP_CONFIG_COLUMNS: &str = r#"
|
||||||
SELECT
|
SELECT
|
||||||
server_url,
|
server_url,
|
||||||
bind_dn,
|
bind_dn,
|
||||||
@@ -36,8 +35,6 @@ SELECT
|
|||||||
use_starttls,
|
use_starttls,
|
||||||
connect_timeout
|
connect_timeout
|
||||||
FROM ldap_configs
|
FROM ldap_configs
|
||||||
ORDER BY id ASC
|
|
||||||
LIMIT 1
|
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -62,24 +59,37 @@ 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]
|
#[async_trait]
|
||||||
impl AuthModuleReadRepository for MysqlAuthModuleReadRepository {
|
impl AuthModuleReadRepository for MysqlAuthModuleReadRepository {
|
||||||
async fn list_enabled_oauth_providers(
|
async fn list_enabled_oauth_providers(
|
||||||
&self,
|
&self,
|
||||||
) -> Result<Vec<StoredOAuthProviderModuleConfig>, DataLayerError> {
|
) -> Result<Vec<StoredOAuthProviderModuleConfig>, DataLayerError> {
|
||||||
let rows = sqlx::query(LIST_ENABLED_OAUTH_PROVIDERS_SQL)
|
list_enabled_oauth_providers(&self.pool).await
|
||||||
.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> {
|
async fn get_ldap_config(&self) -> Result<Option<StoredLdapModuleConfig>, DataLayerError> {
|
||||||
let row = sqlx::query(GET_LDAP_CONFIG_SQL)
|
get_ldap_config(&self.pool).await
|
||||||
.fetch_optional(&self.pool)
|
|
||||||
.await
|
|
||||||
.map_sql_err()?;
|
|
||||||
row.as_ref().map(map_ldap_row).transpose()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,19 +98,11 @@ impl AuthModuleReadRepository for MysqlAuthModuleRepository {
|
|||||||
async fn list_enabled_oauth_providers(
|
async fn list_enabled_oauth_providers(
|
||||||
&self,
|
&self,
|
||||||
) -> Result<Vec<StoredOAuthProviderModuleConfig>, DataLayerError> {
|
) -> Result<Vec<StoredOAuthProviderModuleConfig>, DataLayerError> {
|
||||||
let rows = sqlx::query(LIST_ENABLED_OAUTH_PROVIDERS_SQL)
|
list_enabled_oauth_providers(&self.pool).await
|
||||||
.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> {
|
async fn get_ldap_config(&self) -> Result<Option<StoredLdapModuleConfig>, DataLayerError> {
|
||||||
let row = sqlx::query(GET_LDAP_CONFIG_SQL)
|
get_ldap_config(&self.pool).await
|
||||||
.fetch_optional(&self.pool)
|
|
||||||
.await
|
|
||||||
.map_sql_err()?;
|
|
||||||
row.as_ref().map(map_ldap_row).transpose()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use futures_util::{stream::TryStream, TryStreamExt};
|
use sqlx::{postgres::PgRow, PgPool, Postgres, QueryBuilder, Row};
|
||||||
use sqlx::{postgres::PgRow, PgPool, Row};
|
|
||||||
|
|
||||||
use super::types::{
|
use super::types::{
|
||||||
AuthModuleReadRepository, AuthModuleWriteRepository, StoredLdapModuleConfig,
|
AuthModuleReadRepository, AuthModuleWriteRepository, StoredLdapModuleConfig,
|
||||||
StoredOAuthProviderModuleConfig,
|
StoredOAuthProviderModuleConfig,
|
||||||
};
|
};
|
||||||
use crate::{error::SqlxResultExt, DataLayerError};
|
use crate::{error::SqlxResultExt, DataLayerError};
|
||||||
|
use aether_data_query::{push_eq, push_limit, WhereClause};
|
||||||
|
|
||||||
const LIST_ENABLED_OAUTH_PROVIDERS_SQL: &str = r#"
|
const OAUTH_PROVIDER_COLUMNS: &str = r#"
|
||||||
SELECT
|
SELECT
|
||||||
provider_type,
|
provider_type,
|
||||||
display_name,
|
display_name,
|
||||||
@@ -16,11 +16,9 @@ SELECT
|
|||||||
client_secret_encrypted,
|
client_secret_encrypted,
|
||||||
redirect_uri
|
redirect_uri
|
||||||
FROM oauth_providers
|
FROM oauth_providers
|
||||||
WHERE is_enabled = TRUE
|
|
||||||
ORDER BY provider_type ASC
|
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
const GET_LDAP_CONFIG_SQL: &str = r#"
|
const LDAP_CONFIG_COLUMNS: &str = r#"
|
||||||
SELECT
|
SELECT
|
||||||
server_url,
|
server_url,
|
||||||
bind_dn,
|
bind_dn,
|
||||||
@@ -35,8 +33,6 @@ SELECT
|
|||||||
use_starttls,
|
use_starttls,
|
||||||
connect_timeout
|
connect_timeout
|
||||||
FROM ldap_configs
|
FROM ldap_configs
|
||||||
ORDER BY id ASC
|
|
||||||
LIMIT 1
|
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
const UPDATE_LDAP_CONFIG_SQL: &str = r#"
|
const UPDATE_LDAP_CONFIG_SQL: &str = r#"
|
||||||
@@ -146,18 +142,27 @@ impl SqlxAuthModuleRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn collect_query_rows<T, S>(
|
async fn list_enabled_oauth_providers(
|
||||||
mut rows: S,
|
pool: &PgPool,
|
||||||
map_row: fn(&PgRow) -> Result<T, DataLayerError>,
|
) -> Result<Vec<StoredOAuthProviderModuleConfig>, DataLayerError> {
|
||||||
) -> Result<Vec<T>, DataLayerError>
|
let mut builder = QueryBuilder::<Postgres>::new(OAUTH_PROVIDER_COLUMNS);
|
||||||
where
|
let mut where_clause = WhereClause::new();
|
||||||
S: TryStream<Ok = PgRow, Error = sqlx::Error> + Unpin,
|
push_eq(&mut builder, &mut where_clause, "is_enabled", true);
|
||||||
{
|
builder.push(" ORDER BY provider_type ASC");
|
||||||
let mut items = Vec::new();
|
let rows = builder.build().fetch_all(pool).await.map_postgres_err()?;
|
||||||
while let Some(row) = rows.try_next().await.map_postgres_err()? {
|
rows.iter().map(map_oauth_row).collect()
|
||||||
items.push(map_row(&row)?);
|
|
||||||
}
|
}
|
||||||
Ok(items)
|
|
||||||
|
async fn get_ldap_config(pool: &PgPool) -> Result<Option<StoredLdapModuleConfig>, DataLayerError> {
|
||||||
|
let mut builder = QueryBuilder::<Postgres>::new(LDAP_CONFIG_COLUMNS);
|
||||||
|
builder.push(" ORDER BY id ASC");
|
||||||
|
push_limit(&mut builder, 1);
|
||||||
|
let row = builder
|
||||||
|
.build()
|
||||||
|
.fetch_optional(pool)
|
||||||
|
.await
|
||||||
|
.map_postgres_err()?;
|
||||||
|
row.as_ref().map(map_ldap_row).transpose()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -165,19 +170,11 @@ impl AuthModuleReadRepository for SqlxAuthModuleReadRepository {
|
|||||||
async fn list_enabled_oauth_providers(
|
async fn list_enabled_oauth_providers(
|
||||||
&self,
|
&self,
|
||||||
) -> Result<Vec<StoredOAuthProviderModuleConfig>, DataLayerError> {
|
) -> Result<Vec<StoredOAuthProviderModuleConfig>, DataLayerError> {
|
||||||
collect_query_rows(
|
list_enabled_oauth_providers(&self.pool).await
|
||||||
sqlx::query(LIST_ENABLED_OAUTH_PROVIDERS_SQL).fetch(&self.pool),
|
|
||||||
map_oauth_row,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_ldap_config(&self) -> Result<Option<StoredLdapModuleConfig>, DataLayerError> {
|
async fn get_ldap_config(&self) -> Result<Option<StoredLdapModuleConfig>, DataLayerError> {
|
||||||
let row = sqlx::query(GET_LDAP_CONFIG_SQL)
|
get_ldap_config(&self.pool).await
|
||||||
.fetch_optional(&self.pool)
|
|
||||||
.await
|
|
||||||
.map_postgres_err()?;
|
|
||||||
row.as_ref().map(map_ldap_row).transpose()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,19 +183,11 @@ impl AuthModuleReadRepository for SqlxAuthModuleRepository {
|
|||||||
async fn list_enabled_oauth_providers(
|
async fn list_enabled_oauth_providers(
|
||||||
&self,
|
&self,
|
||||||
) -> Result<Vec<StoredOAuthProviderModuleConfig>, DataLayerError> {
|
) -> Result<Vec<StoredOAuthProviderModuleConfig>, DataLayerError> {
|
||||||
collect_query_rows(
|
list_enabled_oauth_providers(&self.pool).await
|
||||||
sqlx::query(LIST_ENABLED_OAUTH_PROVIDERS_SQL).fetch(&self.pool),
|
|
||||||
map_oauth_row,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_ldap_config(&self) -> Result<Option<StoredLdapModuleConfig>, DataLayerError> {
|
async fn get_ldap_config(&self) -> Result<Option<StoredLdapModuleConfig>, DataLayerError> {
|
||||||
let row = sqlx::query(GET_LDAP_CONFIG_SQL)
|
get_ldap_config(&self.pool).await
|
||||||
.fetch_optional(&self.pool)
|
|
||||||
.await
|
|
||||||
.map_postgres_err()?;
|
|
||||||
row.as_ref().map(map_ldap_row).transpose()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use sqlx::{sqlite::SqliteRow, Row};
|
use sqlx::{sqlite::SqliteRow, QueryBuilder, Row, Sqlite};
|
||||||
|
|
||||||
use super::types::{
|
use super::types::{
|
||||||
AuthModuleReadRepository, AuthModuleWriteRepository, StoredLdapModuleConfig,
|
AuthModuleReadRepository, AuthModuleWriteRepository, StoredLdapModuleConfig,
|
||||||
@@ -8,8 +8,9 @@ use super::types::{
|
|||||||
use crate::driver::sqlite::SqlitePool;
|
use crate::driver::sqlite::SqlitePool;
|
||||||
use crate::error::SqlResultExt;
|
use crate::error::SqlResultExt;
|
||||||
use crate::DataLayerError;
|
use crate::DataLayerError;
|
||||||
|
use aether_data_query::{push_eq, push_limit, WhereClause};
|
||||||
|
|
||||||
const LIST_ENABLED_OAUTH_PROVIDERS_SQL: &str = r#"
|
const OAUTH_PROVIDER_COLUMNS: &str = r#"
|
||||||
SELECT
|
SELECT
|
||||||
provider_type,
|
provider_type,
|
||||||
display_name,
|
display_name,
|
||||||
@@ -17,11 +18,9 @@ SELECT
|
|||||||
client_secret_encrypted,
|
client_secret_encrypted,
|
||||||
redirect_uri
|
redirect_uri
|
||||||
FROM oauth_providers
|
FROM oauth_providers
|
||||||
WHERE is_enabled = 1
|
|
||||||
ORDER BY provider_type ASC
|
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
const GET_LDAP_CONFIG_SQL: &str = r#"
|
const LDAP_CONFIG_COLUMNS: &str = r#"
|
||||||
SELECT
|
SELECT
|
||||||
server_url,
|
server_url,
|
||||||
bind_dn,
|
bind_dn,
|
||||||
@@ -36,8 +35,6 @@ SELECT
|
|||||||
use_starttls,
|
use_starttls,
|
||||||
connect_timeout
|
connect_timeout
|
||||||
FROM ldap_configs
|
FROM ldap_configs
|
||||||
ORDER BY id ASC
|
|
||||||
LIMIT 1
|
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -62,24 +59,37 @@ impl SqliteAuthModuleRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn list_enabled_oauth_providers(
|
||||||
|
pool: &SqlitePool,
|
||||||
|
) -> Result<Vec<StoredOAuthProviderModuleConfig>, DataLayerError> {
|
||||||
|
let mut builder = QueryBuilder::<Sqlite>::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: &SqlitePool,
|
||||||
|
) -> Result<Option<StoredLdapModuleConfig>, DataLayerError> {
|
||||||
|
let mut builder = QueryBuilder::<Sqlite>::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]
|
#[async_trait]
|
||||||
impl AuthModuleReadRepository for SqliteAuthModuleReadRepository {
|
impl AuthModuleReadRepository for SqliteAuthModuleReadRepository {
|
||||||
async fn list_enabled_oauth_providers(
|
async fn list_enabled_oauth_providers(
|
||||||
&self,
|
&self,
|
||||||
) -> Result<Vec<StoredOAuthProviderModuleConfig>, DataLayerError> {
|
) -> Result<Vec<StoredOAuthProviderModuleConfig>, DataLayerError> {
|
||||||
let rows = sqlx::query(LIST_ENABLED_OAUTH_PROVIDERS_SQL)
|
list_enabled_oauth_providers(&self.pool).await
|
||||||
.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> {
|
async fn get_ldap_config(&self) -> Result<Option<StoredLdapModuleConfig>, DataLayerError> {
|
||||||
let row = sqlx::query(GET_LDAP_CONFIG_SQL)
|
get_ldap_config(&self.pool).await
|
||||||
.fetch_optional(&self.pool)
|
|
||||||
.await
|
|
||||||
.map_sql_err()?;
|
|
||||||
row.as_ref().map(map_ldap_row).transpose()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,19 +98,11 @@ impl AuthModuleReadRepository for SqliteAuthModuleRepository {
|
|||||||
async fn list_enabled_oauth_providers(
|
async fn list_enabled_oauth_providers(
|
||||||
&self,
|
&self,
|
||||||
) -> Result<Vec<StoredOAuthProviderModuleConfig>, DataLayerError> {
|
) -> Result<Vec<StoredOAuthProviderModuleConfig>, DataLayerError> {
|
||||||
let rows = sqlx::query(LIST_ENABLED_OAUTH_PROVIDERS_SQL)
|
list_enabled_oauth_providers(&self.pool).await
|
||||||
.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> {
|
async fn get_ldap_config(&self) -> Result<Option<StoredLdapModuleConfig>, DataLayerError> {
|
||||||
let row = sqlx::query(GET_LDAP_CONFIG_SQL)
|
get_ldap_config(&self.pool).await
|
||||||
.fetch_optional(&self.pool)
|
|
||||||
.await
|
|
||||||
.map_sql_err()?;
|
|
||||||
row.as_ref().map(map_ldap_row).transpose()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ use super::{
|
|||||||
use crate::driver::mysql::MysqlPool;
|
use crate::driver::mysql::MysqlPool;
|
||||||
use crate::error::SqlResultExt;
|
use crate::error::SqlResultExt;
|
||||||
use crate::DataLayerError;
|
use crate::DataLayerError;
|
||||||
|
use aether_data_query::{
|
||||||
|
push_ci_contains, push_eq, push_limit, push_limit_offset, SqlDialect, WhereClause,
|
||||||
|
};
|
||||||
|
|
||||||
const RUN_COLUMNS: &str = r#"
|
const RUN_COLUMNS: &str = r#"
|
||||||
SELECT
|
SELECT
|
||||||
@@ -57,44 +60,29 @@ impl MysqlBackgroundTaskRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn apply_run_filter(builder: &mut QueryBuilder<'_, MySql>, query: &BackgroundTaskListQuery) {
|
fn apply_run_filter(builder: &mut QueryBuilder<'_, MySql>, query: &BackgroundTaskListQuery) {
|
||||||
let mut has_where = false;
|
let mut where_clause = WhereClause::new();
|
||||||
if let Some(kind) = query.kind {
|
if let Some(kind) = query.kind {
|
||||||
if !has_where {
|
push_eq(builder, &mut where_clause, "kind", kind.as_database());
|
||||||
builder.push(" WHERE ");
|
|
||||||
has_where = true;
|
|
||||||
} else {
|
|
||||||
builder.push(" AND ");
|
|
||||||
}
|
|
||||||
builder.push("kind = ").push_bind(kind.as_database());
|
|
||||||
}
|
}
|
||||||
if let Some(status) = query.status {
|
if let Some(status) = query.status {
|
||||||
if !has_where {
|
push_eq(builder, &mut where_clause, "status", status.as_database());
|
||||||
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() {
|
if let Some(trigger) = query.trigger.as_deref() {
|
||||||
if !has_where {
|
push_eq(
|
||||||
builder.push(" WHERE ");
|
builder,
|
||||||
has_where = true;
|
&mut where_clause,
|
||||||
} else {
|
&SqlDialect::Mysql.quote_ident("trigger"),
|
||||||
builder.push(" AND ");
|
trigger.to_string(),
|
||||||
}
|
);
|
||||||
builder.push("`trigger` = ").push_bind(trigger.to_string());
|
|
||||||
}
|
}
|
||||||
if let Some(task_key_substring) = query.task_key_substring.as_deref() {
|
if let Some(task_key_substring) = query.task_key_substring.as_deref() {
|
||||||
if !has_where {
|
push_ci_contains(
|
||||||
builder.push(" WHERE ");
|
builder,
|
||||||
} else {
|
&mut where_clause,
|
||||||
builder.push(" AND ");
|
SqlDialect::Mysql,
|
||||||
}
|
"task_key",
|
||||||
builder.push("LOWER(task_key) LIKE ").push_bind(format!(
|
task_key_substring,
|
||||||
"%{}%",
|
);
|
||||||
task_key_substring.trim().to_ascii_lowercase()
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -105,8 +93,12 @@ impl BackgroundTaskReadRepository for MysqlBackgroundTaskRepository {
|
|||||||
&self,
|
&self,
|
||||||
run_id: &str,
|
run_id: &str,
|
||||||
) -> Result<Option<StoredBackgroundTaskRun>, DataLayerError> {
|
) -> Result<Option<StoredBackgroundTaskRun>, DataLayerError> {
|
||||||
let row = sqlx::query(&format!("{RUN_COLUMNS} WHERE id = ? LIMIT 1"))
|
let mut builder = QueryBuilder::<MySql>::new(RUN_COLUMNS);
|
||||||
.bind(run_id)
|
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()
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_sql_err()?;
|
.map_sql_err()?;
|
||||||
@@ -129,12 +121,12 @@ impl BackgroundTaskReadRepository for MysqlBackgroundTaskRepository {
|
|||||||
|
|
||||||
let mut builder = QueryBuilder::<MySql>::new(RUN_COLUMNS);
|
let mut builder = QueryBuilder::<MySql>::new(RUN_COLUMNS);
|
||||||
Self::apply_run_filter(&mut builder, query);
|
Self::apply_run_filter(&mut builder, query);
|
||||||
builder
|
builder.push(" ORDER BY created_at_unix_secs DESC, updated_at_unix_secs DESC");
|
||||||
.push(" ORDER BY created_at_unix_secs DESC, updated_at_unix_secs DESC")
|
push_limit_offset(
|
||||||
.push(" LIMIT ")
|
&mut builder,
|
||||||
.push_bind(i64_from_usize(limit, "run limit")?)
|
i64_from_usize(limit, "run limit")?,
|
||||||
.push(" OFFSET ")
|
i64_from_usize(query.offset, "run offset")?,
|
||||||
.push_bind(i64_from_usize(query.offset, "run offset")?);
|
);
|
||||||
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
||||||
let items = rows
|
let items = rows
|
||||||
.iter()
|
.iter()
|
||||||
@@ -153,15 +145,21 @@ impl BackgroundTaskReadRepository for MysqlBackgroundTaskRepository {
|
|||||||
limit: usize,
|
limit: usize,
|
||||||
) -> Result<Vec<StoredBackgroundTaskEvent>, DataLayerError> {
|
) -> Result<Vec<StoredBackgroundTaskEvent>, DataLayerError> {
|
||||||
let limit = limit.max(1);
|
let limit = limit.max(1);
|
||||||
let rows = sqlx::query(&format!(
|
let mut builder = QueryBuilder::<MySql>::new(EVENT_COLUMNS);
|
||||||
"{EVENT_COLUMNS} WHERE run_id = ? ORDER BY created_at_unix_secs ASC, id ASC LIMIT ? OFFSET ?"
|
let mut where_clause = WhereClause::new();
|
||||||
))
|
push_eq(
|
||||||
.bind(run_id)
|
&mut builder,
|
||||||
.bind(i64_from_usize(limit, "event limit")?)
|
&mut where_clause,
|
||||||
.bind(i64_from_usize(offset, "event offset")?)
|
"run_id",
|
||||||
.fetch_all(&self.pool)
|
run_id.to_string(),
|
||||||
.await
|
);
|
||||||
.map_sql_err()?;
|
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()?;
|
||||||
rows.iter().map(map_event_row).collect()
|
rows.iter().map(map_event_row).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ use super::{
|
|||||||
};
|
};
|
||||||
use crate::error::SqlxResultExt;
|
use crate::error::SqlxResultExt;
|
||||||
use crate::DataLayerError;
|
use crate::DataLayerError;
|
||||||
|
use aether_data_query::{
|
||||||
|
push_ci_contains, push_eq, push_limit, push_limit_offset, SqlDialect, WhereClause,
|
||||||
|
};
|
||||||
|
|
||||||
const RUN_COLUMNS: &str = r#"
|
const RUN_COLUMNS: &str = r#"
|
||||||
SELECT
|
SELECT
|
||||||
@@ -60,35 +63,33 @@ impl SqlxBackgroundTaskRepository {
|
|||||||
query: &BackgroundTaskListQuery,
|
query: &BackgroundTaskListQuery,
|
||||||
include_where: bool,
|
include_where: bool,
|
||||||
) {
|
) {
|
||||||
let mut has_where = include_where;
|
let mut where_clause = if include_where {
|
||||||
let mut push_where = |builder: &mut QueryBuilder<'_, Postgres>| {
|
WhereClause::with_existing_clause()
|
||||||
if has_where {
|
|
||||||
builder.push(" AND ");
|
|
||||||
} else {
|
} else {
|
||||||
builder.push(" WHERE ");
|
WhereClause::new()
|
||||||
has_where = true;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(kind) = query.kind {
|
if let Some(kind) = query.kind {
|
||||||
push_where(builder);
|
push_eq(builder, &mut where_clause, "kind", kind.as_database());
|
||||||
builder.push("kind = ").push_bind(kind.as_database());
|
|
||||||
}
|
}
|
||||||
if let Some(status) = query.status {
|
if let Some(status) = query.status {
|
||||||
push_where(builder);
|
push_eq(builder, &mut where_clause, "status", status.as_database());
|
||||||
builder.push("status = ").push_bind(status.as_database());
|
|
||||||
}
|
}
|
||||||
if let Some(trigger) = query.trigger.as_deref() {
|
if let Some(trigger) = query.trigger.as_deref() {
|
||||||
push_where(builder);
|
push_eq(
|
||||||
builder
|
builder,
|
||||||
.push("\"trigger\" = ")
|
&mut where_clause,
|
||||||
.push_bind(trigger.to_string());
|
&SqlDialect::Postgres.quote_ident("trigger"),
|
||||||
|
trigger.to_string(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if let Some(task_key_substring) = query.task_key_substring.as_deref() {
|
if let Some(task_key_substring) = query.task_key_substring.as_deref() {
|
||||||
push_where(builder);
|
push_ci_contains(
|
||||||
builder
|
builder,
|
||||||
.push("task_key ILIKE ")
|
&mut where_clause,
|
||||||
.push_bind(format!("%{}%", task_key_substring.trim()));
|
SqlDialect::Postgres,
|
||||||
|
"task_key",
|
||||||
|
task_key_substring,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,8 +100,12 @@ impl BackgroundTaskReadRepository for SqlxBackgroundTaskRepository {
|
|||||||
&self,
|
&self,
|
||||||
run_id: &str,
|
run_id: &str,
|
||||||
) -> Result<Option<StoredBackgroundTaskRun>, DataLayerError> {
|
) -> Result<Option<StoredBackgroundTaskRun>, DataLayerError> {
|
||||||
let row = sqlx::query(&format!("{RUN_COLUMNS} WHERE id = $1 LIMIT 1"))
|
let mut builder = QueryBuilder::<Postgres>::new(RUN_COLUMNS);
|
||||||
.bind(run_id)
|
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()
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_postgres_err()?;
|
.map_postgres_err()?;
|
||||||
@@ -124,12 +129,12 @@ impl BackgroundTaskReadRepository for SqlxBackgroundTaskRepository {
|
|||||||
|
|
||||||
let mut builder = QueryBuilder::<Postgres>::new(RUN_COLUMNS);
|
let mut builder = QueryBuilder::<Postgres>::new(RUN_COLUMNS);
|
||||||
Self::apply_run_filter(&mut builder, query, false);
|
Self::apply_run_filter(&mut builder, query, false);
|
||||||
builder
|
builder.push(" ORDER BY created_at_unix_secs DESC, updated_at_unix_secs DESC");
|
||||||
.push(" ORDER BY created_at_unix_secs DESC, updated_at_unix_secs DESC")
|
push_limit_offset(
|
||||||
.push(" LIMIT ")
|
&mut builder,
|
||||||
.push_bind(i64_from_usize(limit, "background task run limit")?)
|
i64_from_usize(limit, "background task run limit")?,
|
||||||
.push(" OFFSET ")
|
i64_from_usize(query.offset, "background task run offset")?,
|
||||||
.push_bind(i64_from_usize(query.offset, "background task run offset")?);
|
);
|
||||||
let rows = builder
|
let rows = builder
|
||||||
.build()
|
.build()
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
@@ -153,12 +158,22 @@ impl BackgroundTaskReadRepository for SqlxBackgroundTaskRepository {
|
|||||||
limit: usize,
|
limit: usize,
|
||||||
) -> Result<Vec<StoredBackgroundTaskEvent>, DataLayerError> {
|
) -> Result<Vec<StoredBackgroundTaskEvent>, DataLayerError> {
|
||||||
let limit = limit.max(1);
|
let limit = limit.max(1);
|
||||||
let rows = sqlx::query(&format!(
|
let mut builder = QueryBuilder::<Postgres>::new(EVENT_COLUMNS);
|
||||||
"{EVENT_COLUMNS} WHERE run_id = $1 ORDER BY created_at_unix_secs ASC, id ASC LIMIT $2 OFFSET $3"
|
let mut where_clause = WhereClause::new();
|
||||||
))
|
push_eq(
|
||||||
.bind(run_id)
|
&mut builder,
|
||||||
.bind(i64_from_usize(limit, "background task event limit")?)
|
&mut where_clause,
|
||||||
.bind(i64_from_usize(offset, "background task event offset")?)
|
"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, "background task event limit")?,
|
||||||
|
i64_from_usize(offset, "background task event offset")?,
|
||||||
|
);
|
||||||
|
let rows = builder
|
||||||
|
.build()
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_postgres_err()?;
|
.map_postgres_err()?;
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ use super::{
|
|||||||
use crate::driver::sqlite::SqlitePool;
|
use crate::driver::sqlite::SqlitePool;
|
||||||
use crate::error::SqlResultExt;
|
use crate::error::SqlResultExt;
|
||||||
use crate::DataLayerError;
|
use crate::DataLayerError;
|
||||||
|
use aether_data_query::{
|
||||||
|
push_ci_contains, push_eq, push_limit, push_limit_offset, SqlDialect, WhereClause,
|
||||||
|
};
|
||||||
|
|
||||||
const RUN_COLUMNS: &str = r#"
|
const RUN_COLUMNS: &str = r#"
|
||||||
SELECT
|
SELECT
|
||||||
@@ -57,46 +60,29 @@ impl SqliteBackgroundTaskRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn apply_run_filter(builder: &mut QueryBuilder<'_, Sqlite>, query: &BackgroundTaskListQuery) {
|
fn apply_run_filter(builder: &mut QueryBuilder<'_, Sqlite>, query: &BackgroundTaskListQuery) {
|
||||||
let mut has_where = false;
|
let mut where_clause = WhereClause::new();
|
||||||
if let Some(kind) = query.kind {
|
if let Some(kind) = query.kind {
|
||||||
if !has_where {
|
push_eq(builder, &mut where_clause, "kind", kind.as_database());
|
||||||
builder.push(" WHERE ");
|
|
||||||
has_where = true;
|
|
||||||
} else {
|
|
||||||
builder.push(" AND ");
|
|
||||||
}
|
|
||||||
builder.push("kind = ").push_bind(kind.as_database());
|
|
||||||
}
|
}
|
||||||
if let Some(status) = query.status {
|
if let Some(status) = query.status {
|
||||||
if !has_where {
|
push_eq(builder, &mut where_clause, "status", status.as_database());
|
||||||
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() {
|
if let Some(trigger) = query.trigger.as_deref() {
|
||||||
if !has_where {
|
push_eq(
|
||||||
builder.push(" WHERE ");
|
builder,
|
||||||
has_where = true;
|
&mut where_clause,
|
||||||
} else {
|
&SqlDialect::Sqlite.quote_ident("trigger"),
|
||||||
builder.push(" AND ");
|
trigger.to_string(),
|
||||||
}
|
);
|
||||||
builder
|
|
||||||
.push("\"trigger\" = ")
|
|
||||||
.push_bind(trigger.to_string());
|
|
||||||
}
|
}
|
||||||
if let Some(task_key_substring) = query.task_key_substring.as_deref() {
|
if let Some(task_key_substring) = query.task_key_substring.as_deref() {
|
||||||
if !has_where {
|
push_ci_contains(
|
||||||
builder.push(" WHERE ");
|
builder,
|
||||||
} else {
|
&mut where_clause,
|
||||||
builder.push(" AND ");
|
SqlDialect::Sqlite,
|
||||||
}
|
"task_key",
|
||||||
builder.push("LOWER(task_key) LIKE ").push_bind(format!(
|
task_key_substring,
|
||||||
"%{}%",
|
);
|
||||||
task_key_substring.trim().to_ascii_lowercase()
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,8 +93,12 @@ impl BackgroundTaskReadRepository for SqliteBackgroundTaskRepository {
|
|||||||
&self,
|
&self,
|
||||||
run_id: &str,
|
run_id: &str,
|
||||||
) -> Result<Option<StoredBackgroundTaskRun>, DataLayerError> {
|
) -> Result<Option<StoredBackgroundTaskRun>, DataLayerError> {
|
||||||
let row = sqlx::query(&format!("{RUN_COLUMNS} WHERE id = ? LIMIT 1"))
|
let mut builder = QueryBuilder::<Sqlite>::new(RUN_COLUMNS);
|
||||||
.bind(run_id)
|
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()
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_sql_err()?;
|
.map_sql_err()?;
|
||||||
@@ -131,12 +121,12 @@ impl BackgroundTaskReadRepository for SqliteBackgroundTaskRepository {
|
|||||||
|
|
||||||
let mut builder = QueryBuilder::<Sqlite>::new(RUN_COLUMNS);
|
let mut builder = QueryBuilder::<Sqlite>::new(RUN_COLUMNS);
|
||||||
Self::apply_run_filter(&mut builder, query);
|
Self::apply_run_filter(&mut builder, query);
|
||||||
builder
|
builder.push(" ORDER BY created_at_unix_secs DESC, updated_at_unix_secs DESC");
|
||||||
.push(" ORDER BY created_at_unix_secs DESC, updated_at_unix_secs DESC")
|
push_limit_offset(
|
||||||
.push(" LIMIT ")
|
&mut builder,
|
||||||
.push_bind(i64_from_usize(limit, "run limit")?)
|
i64_from_usize(limit, "run limit")?,
|
||||||
.push(" OFFSET ")
|
i64_from_usize(query.offset, "run offset")?,
|
||||||
.push_bind(i64_from_usize(query.offset, "run offset")?);
|
);
|
||||||
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
||||||
let items = rows
|
let items = rows
|
||||||
.iter()
|
.iter()
|
||||||
@@ -155,15 +145,21 @@ impl BackgroundTaskReadRepository for SqliteBackgroundTaskRepository {
|
|||||||
limit: usize,
|
limit: usize,
|
||||||
) -> Result<Vec<StoredBackgroundTaskEvent>, DataLayerError> {
|
) -> Result<Vec<StoredBackgroundTaskEvent>, DataLayerError> {
|
||||||
let limit = limit.max(1);
|
let limit = limit.max(1);
|
||||||
let rows = sqlx::query(&format!(
|
let mut builder = QueryBuilder::<Sqlite>::new(EVENT_COLUMNS);
|
||||||
"{EVENT_COLUMNS} WHERE run_id = ? ORDER BY created_at_unix_secs ASC, id ASC LIMIT ? OFFSET ?"
|
let mut where_clause = WhereClause::new();
|
||||||
))
|
push_eq(
|
||||||
.bind(run_id)
|
&mut builder,
|
||||||
.bind(i64_from_usize(limit, "event limit")?)
|
&mut where_clause,
|
||||||
.bind(i64_from_usize(offset, "event offset")?)
|
"run_id",
|
||||||
.fetch_all(&self.pool)
|
run_id.to_string(),
|
||||||
.await
|
);
|
||||||
.map_sql_err()?;
|
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()?;
|
||||||
rows.iter().map(map_event_row).collect()
|
rows.iter().map(map_event_row).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use super::{
|
|||||||
use crate::driver::mysql::MysqlPool;
|
use crate::driver::mysql::MysqlPool;
|
||||||
use crate::error::SqlResultExt;
|
use crate::error::SqlResultExt;
|
||||||
use crate::DataLayerError;
|
use crate::DataLayerError;
|
||||||
|
use aether_data_query::{push_in, WhereClause};
|
||||||
|
|
||||||
const CANDIDATE_COLUMNS: &str = r#"
|
const CANDIDATE_COLUMNS: &str = r#"
|
||||||
SELECT
|
SELECT
|
||||||
@@ -132,7 +133,8 @@ impl RequestCandidateReadRepository for MysqlRequestCandidateRepository {
|
|||||||
return Ok(Vec::new());
|
return Ok(Vec::new());
|
||||||
}
|
}
|
||||||
let mut builder = QueryBuilder::<MySql>::new(CANDIDATE_COLUMNS);
|
let mut builder = QueryBuilder::<MySql>::new(CANDIDATE_COLUMNS);
|
||||||
push_endpoint_in_clause(&mut builder, endpoint_ids);
|
let mut where_clause = WhereClause::new();
|
||||||
|
push_in(&mut builder, &mut where_clause, "endpoint_id", endpoint_ids);
|
||||||
builder
|
builder
|
||||||
.push(" AND created_at >= ")
|
.push(" AND created_at >= ")
|
||||||
.push_bind(unix_secs_to_ms_i64(since_unix_secs)?)
|
.push_bind(unix_secs_to_ms_i64(since_unix_secs)?)
|
||||||
@@ -154,7 +156,8 @@ impl RequestCandidateReadRepository for MysqlRequestCandidateRepository {
|
|||||||
let mut builder = QueryBuilder::<MySql>::new(
|
let mut builder = QueryBuilder::<MySql>::new(
|
||||||
"SELECT endpoint_id, status, COUNT(id) AS count FROM request_candidates",
|
"SELECT endpoint_id, status, COUNT(id) AS count FROM request_candidates",
|
||||||
);
|
);
|
||||||
push_endpoint_in_clause(&mut builder, endpoint_ids);
|
let mut where_clause = WhereClause::new();
|
||||||
|
push_in(&mut builder, &mut where_clause, "endpoint_id", endpoint_ids);
|
||||||
builder
|
builder
|
||||||
.push(" AND created_at >= ")
|
.push(" AND created_at >= ")
|
||||||
.push_bind(unix_secs_to_ms_i64(since_unix_secs)?)
|
.push_bind(unix_secs_to_ms_i64(since_unix_secs)?)
|
||||||
@@ -193,7 +196,8 @@ impl RequestCandidateReadRepository for MysqlRequestCandidateRepository {
|
|||||||
let since_ms = unix_secs_to_ms_i64(since_unix_secs)?;
|
let since_ms = unix_secs_to_ms_i64(since_unix_secs)?;
|
||||||
let until_ms = unix_secs_to_ms_i64(until_unix_secs)?;
|
let until_ms = unix_secs_to_ms_i64(until_unix_secs)?;
|
||||||
let mut builder = QueryBuilder::<MySql>::new(CANDIDATE_COLUMNS);
|
let mut builder = QueryBuilder::<MySql>::new(CANDIDATE_COLUMNS);
|
||||||
push_endpoint_in_clause(&mut builder, endpoint_ids);
|
let mut where_clause = WhereClause::new();
|
||||||
|
push_in(&mut builder, &mut where_clause, "endpoint_id", endpoint_ids);
|
||||||
builder
|
builder
|
||||||
.push(" AND created_at >= ")
|
.push(" AND created_at >= ")
|
||||||
.push_bind(since_ms)
|
.push_bind(since_ms)
|
||||||
@@ -339,20 +343,6 @@ ON DUPLICATE KEY UPDATE
|
|||||||
Ok(())
|
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(
|
fn merge_candidate(
|
||||||
candidate: UpsertRequestCandidateRecord,
|
candidate: UpsertRequestCandidateRecord,
|
||||||
existing: Option<StoredRequestCandidate>,
|
existing: Option<StoredRequestCandidate>,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use futures_util::{future::BoxFuture, stream::TryStream, TryStreamExt};
|
use futures_util::{future::BoxFuture, stream::TryStream, TryStreamExt};
|
||||||
use sqlx::{postgres::PgRow, PgPool, Row};
|
use sqlx::{postgres::PgRow, PgPool, Postgres, QueryBuilder, Row};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
@@ -10,6 +10,7 @@ use super::{
|
|||||||
};
|
};
|
||||||
use crate::driver::postgres::PostgresTransactionRunner;
|
use crate::driver::postgres::PostgresTransactionRunner;
|
||||||
use crate::{error::SqlxResultExt, DataLayerError};
|
use crate::{error::SqlxResultExt, DataLayerError};
|
||||||
|
use aether_data_query::{push_eq, push_in, push_limit, WhereClause};
|
||||||
|
|
||||||
const LIST_BY_REQUEST_ID_SQL: &str = r#"
|
const LIST_BY_REQUEST_ID_SQL: &str = r#"
|
||||||
SELECT
|
SELECT
|
||||||
@@ -42,115 +43,6 @@ WHERE request_id = $1
|
|||||||
ORDER BY candidate_index ASC, retry_index ASC, created_at ASC
|
ORDER BY candidate_index ASC, retry_index ASC, created_at ASC
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
const LIST_RECENT_SQL: &str = r#"
|
|
||||||
SELECT
|
|
||||||
id,
|
|
||||||
request_id,
|
|
||||||
user_id,
|
|
||||||
api_key_id,
|
|
||||||
username,
|
|
||||||
api_key_name,
|
|
||||||
candidate_index,
|
|
||||||
retry_index,
|
|
||||||
provider_id,
|
|
||||||
endpoint_id,
|
|
||||||
key_id,
|
|
||||||
status,
|
|
||||||
skip_reason,
|
|
||||||
is_cached,
|
|
||||||
status_code,
|
|
||||||
error_type,
|
|
||||||
error_message,
|
|
||||||
latency_ms,
|
|
||||||
concurrent_requests,
|
|
||||||
extra_data,
|
|
||||||
required_capabilities,
|
|
||||||
CAST(EXTRACT(EPOCH FROM created_at) * 1000 AS BIGINT) AS created_at_unix_ms,
|
|
||||||
CAST(EXTRACT(EPOCH FROM started_at) * 1000 AS BIGINT) AS started_at_unix_ms,
|
|
||||||
CAST(EXTRACT(EPOCH FROM finished_at) * 1000 AS BIGINT) AS finished_at_unix_ms
|
|
||||||
FROM request_candidates
|
|
||||||
ORDER BY created_at DESC
|
|
||||||
LIMIT $1
|
|
||||||
"#;
|
|
||||||
|
|
||||||
const LIST_BY_PROVIDER_ID_SQL: &str = r#"
|
|
||||||
SELECT
|
|
||||||
id,
|
|
||||||
request_id,
|
|
||||||
user_id,
|
|
||||||
api_key_id,
|
|
||||||
username,
|
|
||||||
api_key_name,
|
|
||||||
candidate_index,
|
|
||||||
retry_index,
|
|
||||||
provider_id,
|
|
||||||
endpoint_id,
|
|
||||||
key_id,
|
|
||||||
status,
|
|
||||||
skip_reason,
|
|
||||||
is_cached,
|
|
||||||
status_code,
|
|
||||||
error_type,
|
|
||||||
error_message,
|
|
||||||
latency_ms,
|
|
||||||
concurrent_requests,
|
|
||||||
extra_data,
|
|
||||||
required_capabilities,
|
|
||||||
CAST(EXTRACT(EPOCH FROM created_at) * 1000 AS BIGINT) AS created_at_unix_ms,
|
|
||||||
CAST(EXTRACT(EPOCH FROM started_at) * 1000 AS BIGINT) AS started_at_unix_ms,
|
|
||||||
CAST(EXTRACT(EPOCH FROM finished_at) * 1000 AS BIGINT) AS finished_at_unix_ms
|
|
||||||
FROM request_candidates
|
|
||||||
WHERE provider_id = $1
|
|
||||||
ORDER BY created_at DESC
|
|
||||||
LIMIT $2
|
|
||||||
"#;
|
|
||||||
|
|
||||||
const LIST_FINALIZED_BY_ENDPOINT_IDS_SINCE_SQL: &str = r#"
|
|
||||||
SELECT
|
|
||||||
id,
|
|
||||||
request_id,
|
|
||||||
user_id,
|
|
||||||
api_key_id,
|
|
||||||
username,
|
|
||||||
api_key_name,
|
|
||||||
candidate_index,
|
|
||||||
retry_index,
|
|
||||||
provider_id,
|
|
||||||
endpoint_id,
|
|
||||||
key_id,
|
|
||||||
status,
|
|
||||||
skip_reason,
|
|
||||||
is_cached,
|
|
||||||
status_code,
|
|
||||||
error_type,
|
|
||||||
error_message,
|
|
||||||
latency_ms,
|
|
||||||
concurrent_requests,
|
|
||||||
extra_data,
|
|
||||||
required_capabilities,
|
|
||||||
CAST(EXTRACT(EPOCH FROM created_at) * 1000 AS BIGINT) AS created_at_unix_ms,
|
|
||||||
CAST(EXTRACT(EPOCH FROM started_at) * 1000 AS BIGINT) AS started_at_unix_ms,
|
|
||||||
CAST(EXTRACT(EPOCH FROM finished_at) * 1000 AS BIGINT) AS finished_at_unix_ms
|
|
||||||
FROM request_candidates
|
|
||||||
WHERE endpoint_id = ANY($1)
|
|
||||||
AND created_at >= TO_TIMESTAMP($2)
|
|
||||||
AND status IN ('success', 'failed', 'skipped')
|
|
||||||
ORDER BY created_at DESC
|
|
||||||
LIMIT $3
|
|
||||||
"#;
|
|
||||||
|
|
||||||
const COUNT_FINALIZED_STATUSES_BY_ENDPOINT_IDS_SINCE_SQL: &str = r#"
|
|
||||||
SELECT
|
|
||||||
endpoint_id,
|
|
||||||
status,
|
|
||||||
COUNT(id) AS count
|
|
||||||
FROM request_candidates
|
|
||||||
WHERE endpoint_id = ANY($1)
|
|
||||||
AND created_at >= TO_TIMESTAMP($2)
|
|
||||||
AND status IN ('success', 'failed', 'skipped')
|
|
||||||
GROUP BY endpoint_id, status
|
|
||||||
"#;
|
|
||||||
|
|
||||||
const AGGREGATE_FINALIZED_TIMELINE_BY_ENDPOINT_IDS_SINCE_SQL: &str = r#"
|
const AGGREGATE_FINALIZED_TIMELINE_BY_ENDPOINT_IDS_SINCE_SQL: &str = r#"
|
||||||
SELECT
|
SELECT
|
||||||
endpoint_id,
|
endpoint_id,
|
||||||
@@ -325,13 +217,16 @@ impl SqlxRequestCandidateReadRepository {
|
|||||||
&self,
|
&self,
|
||||||
request_id: &str,
|
request_id: &str,
|
||||||
) -> Result<Vec<StoredRequestCandidate>, DataLayerError> {
|
) -> Result<Vec<StoredRequestCandidate>, DataLayerError> {
|
||||||
collect_query_rows(
|
let mut builder = QueryBuilder::<Postgres>::new(candidate_columns());
|
||||||
sqlx::query(LIST_BY_REQUEST_ID_SQL)
|
let mut where_clause = WhereClause::new();
|
||||||
.bind(request_id)
|
push_eq(
|
||||||
.fetch(&self.pool),
|
&mut builder,
|
||||||
map_request_candidate_row,
|
&mut where_clause,
|
||||||
)
|
"request_id",
|
||||||
.await
|
request_id.to_string(),
|
||||||
|
);
|
||||||
|
builder.push(" ORDER BY candidate_index ASC, retry_index ASC, created_at ASC");
|
||||||
|
collect_query_rows(builder.build().fetch(&self.pool), map_request_candidate_row).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_recent(
|
pub async fn list_recent(
|
||||||
@@ -342,17 +237,17 @@ impl SqlxRequestCandidateReadRepository {
|
|||||||
return Ok(Vec::new());
|
return Ok(Vec::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
collect_query_rows(
|
let mut builder = QueryBuilder::<Postgres>::new(candidate_columns());
|
||||||
sqlx::query(LIST_RECENT_SQL)
|
builder.push(" ORDER BY created_at DESC");
|
||||||
.bind(i64::try_from(limit).map_err(|_| {
|
push_limit(
|
||||||
|
&mut builder,
|
||||||
|
i64::try_from(limit).map_err(|_| {
|
||||||
DataLayerError::UnexpectedValue(format!(
|
DataLayerError::UnexpectedValue(format!(
|
||||||
"invalid recent request candidate limit: {limit}"
|
"invalid recent request candidate limit: {limit}"
|
||||||
))
|
))
|
||||||
})?)
|
})?,
|
||||||
.fetch(&self.pool),
|
);
|
||||||
map_request_candidate_row,
|
collect_query_rows(builder.build().fetch(&self.pool), map_request_candidate_row).await
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_by_provider_id(
|
pub async fn list_by_provider_id(
|
||||||
@@ -364,20 +259,24 @@ impl SqlxRequestCandidateReadRepository {
|
|||||||
return Ok(Vec::new());
|
return Ok(Vec::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
let limit_value = i64::try_from(limit).map_err(|_| {
|
let mut builder = QueryBuilder::<Postgres>::new(candidate_columns());
|
||||||
|
let mut where_clause = WhereClause::new();
|
||||||
|
push_eq(
|
||||||
|
&mut builder,
|
||||||
|
&mut where_clause,
|
||||||
|
"provider_id",
|
||||||
|
provider_id.to_string(),
|
||||||
|
);
|
||||||
|
builder.push(" ORDER BY created_at DESC");
|
||||||
|
push_limit(
|
||||||
|
&mut builder,
|
||||||
|
i64::try_from(limit).map_err(|_| {
|
||||||
DataLayerError::UnexpectedValue(format!(
|
DataLayerError::UnexpectedValue(format!(
|
||||||
"invalid provider request candidate limit: {limit}"
|
"invalid provider request candidate limit: {limit}"
|
||||||
))
|
))
|
||||||
})?;
|
})?,
|
||||||
|
);
|
||||||
collect_query_rows(
|
collect_query_rows(builder.build().fetch(&self.pool), map_request_candidate_row).await
|
||||||
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(
|
pub async fn list_finalized_by_endpoint_ids_since(
|
||||||
@@ -390,19 +289,22 @@ impl SqlxRequestCandidateReadRepository {
|
|||||||
return Ok(Vec::new());
|
return Ok(Vec::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
collect_query_rows(
|
let mut builder = QueryBuilder::<Postgres>::new(candidate_columns());
|
||||||
sqlx::query(LIST_FINALIZED_BY_ENDPOINT_IDS_SINCE_SQL)
|
let mut where_clause = WhereClause::new();
|
||||||
.bind(endpoint_ids)
|
push_in(&mut builder, &mut where_clause, "endpoint_id", endpoint_ids);
|
||||||
.bind(since_unix_secs as f64)
|
builder
|
||||||
.bind(i64::try_from(limit).map_err(|_| {
|
.push(" AND created_at >= TO_TIMESTAMP(")
|
||||||
|
.push_bind(since_unix_secs as f64)
|
||||||
|
.push(") AND status IN ('success', 'failed', 'skipped') ORDER BY created_at DESC");
|
||||||
|
push_limit(
|
||||||
|
&mut builder,
|
||||||
|
i64::try_from(limit).map_err(|_| {
|
||||||
DataLayerError::UnexpectedValue(format!(
|
DataLayerError::UnexpectedValue(format!(
|
||||||
"invalid finalized request candidate limit: {limit}"
|
"invalid finalized request candidate limit: {limit}"
|
||||||
))
|
))
|
||||||
})?)
|
})?,
|
||||||
.fetch(&self.pool),
|
);
|
||||||
map_request_candidate_row,
|
collect_query_rows(builder.build().fetch(&self.pool), map_request_candidate_row).await
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn count_finalized_statuses_by_endpoint_ids_since(
|
pub async fn count_finalized_statuses_by_endpoint_ids_since(
|
||||||
@@ -414,29 +316,35 @@ impl SqlxRequestCandidateReadRepository {
|
|||||||
return Ok(Vec::new());
|
return Ok(Vec::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut rows = sqlx::query(COUNT_FINALIZED_STATUSES_BY_ENDPOINT_IDS_SINCE_SQL)
|
let mut builder = QueryBuilder::<Postgres>::new(
|
||||||
.bind(endpoint_ids)
|
"SELECT endpoint_id, status, COUNT(id) AS count FROM request_candidates",
|
||||||
.bind(since_unix_secs as f64)
|
);
|
||||||
.fetch(&self.pool);
|
let mut where_clause = WhereClause::new();
|
||||||
let mut counts = Vec::new();
|
push_in(&mut builder, &mut where_clause, "endpoint_id", endpoint_ids);
|
||||||
while let Some(row) = rows.try_next().await.map_postgres_err()? {
|
builder
|
||||||
let entry = {
|
.push(" AND created_at >= TO_TIMESTAMP(")
|
||||||
let status = RequestCandidateStatus::from_database(
|
.push_bind(since_unix_secs as f64)
|
||||||
row_get::<String>(&row, "status")?.as_str(),
|
.push(") AND status IN ('success', 'failed', 'skipped') GROUP BY endpoint_id, status");
|
||||||
)?;
|
let rows = builder
|
||||||
PublicHealthStatusCount {
|
.build()
|
||||||
endpoint_id: row_get(&row, "endpoint_id")?,
|
.fetch_all(&self.pool)
|
||||||
status,
|
.await
|
||||||
count: u64::try_from(row_get::<i64>(&row, "count")?).map_err(|_| {
|
.map_postgres_err()?;
|
||||||
|
rows.iter()
|
||||||
|
.map(|row| {
|
||||||
|
Ok(PublicHealthStatusCount {
|
||||||
|
endpoint_id: row_get(row, "endpoint_id")?,
|
||||||
|
status: RequestCandidateStatus::from_database(
|
||||||
|
row_get::<String>(row, "status")?.as_str(),
|
||||||
|
)?,
|
||||||
|
count: u64::try_from(row_get::<i64>(row, "count")?).map_err(|_| {
|
||||||
DataLayerError::UnexpectedValue(
|
DataLayerError::UnexpectedValue(
|
||||||
"public health status count out of range".to_string(),
|
"public health status count out of range".to_string(),
|
||||||
)
|
)
|
||||||
})?,
|
})?,
|
||||||
}
|
})
|
||||||
};
|
})
|
||||||
counts.push(entry);
|
.collect()
|
||||||
}
|
|
||||||
Ok(counts)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn aggregate_finalized_timeline_by_endpoint_ids_since(
|
pub async fn aggregate_finalized_timeline_by_endpoint_ids_since(
|
||||||
@@ -725,6 +633,13 @@ where
|
|||||||
row.try_get(column).map_postgres_err()
|
row.try_get(column).map_postgres_err()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn candidate_columns() -> &'static str {
|
||||||
|
LIST_BY_REQUEST_ID_SQL
|
||||||
|
.split_once("WHERE request_id = $1")
|
||||||
|
.map(|(prefix, _)| prefix)
|
||||||
|
.unwrap_or(LIST_BY_REQUEST_ID_SQL)
|
||||||
|
}
|
||||||
|
|
||||||
fn status_to_database(status: RequestCandidateStatus) -> &'static str {
|
fn status_to_database(status: RequestCandidateStatus) -> &'static str {
|
||||||
match status {
|
match status {
|
||||||
RequestCandidateStatus::Available => "available",
|
RequestCandidateStatus::Available => "available",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use super::{
|
|||||||
use crate::driver::sqlite::SqlitePool;
|
use crate::driver::sqlite::SqlitePool;
|
||||||
use crate::error::SqlResultExt;
|
use crate::error::SqlResultExt;
|
||||||
use crate::DataLayerError;
|
use crate::DataLayerError;
|
||||||
|
use aether_data_query::{push_in, WhereClause};
|
||||||
|
|
||||||
const CANDIDATE_COLUMNS: &str = r#"
|
const CANDIDATE_COLUMNS: &str = r#"
|
||||||
SELECT
|
SELECT
|
||||||
@@ -132,7 +133,8 @@ impl RequestCandidateReadRepository for SqliteRequestCandidateRepository {
|
|||||||
return Ok(Vec::new());
|
return Ok(Vec::new());
|
||||||
}
|
}
|
||||||
let mut builder = QueryBuilder::<Sqlite>::new(CANDIDATE_COLUMNS);
|
let mut builder = QueryBuilder::<Sqlite>::new(CANDIDATE_COLUMNS);
|
||||||
push_endpoint_in_clause(&mut builder, endpoint_ids);
|
let mut where_clause = WhereClause::new();
|
||||||
|
push_in(&mut builder, &mut where_clause, "endpoint_id", endpoint_ids);
|
||||||
builder
|
builder
|
||||||
.push(" AND created_at >= ")
|
.push(" AND created_at >= ")
|
||||||
.push_bind(unix_secs_to_ms_i64(since_unix_secs)?)
|
.push_bind(unix_secs_to_ms_i64(since_unix_secs)?)
|
||||||
@@ -154,7 +156,8 @@ impl RequestCandidateReadRepository for SqliteRequestCandidateRepository {
|
|||||||
let mut builder = QueryBuilder::<Sqlite>::new(
|
let mut builder = QueryBuilder::<Sqlite>::new(
|
||||||
"SELECT endpoint_id, status, COUNT(id) AS count FROM request_candidates",
|
"SELECT endpoint_id, status, COUNT(id) AS count FROM request_candidates",
|
||||||
);
|
);
|
||||||
push_endpoint_in_clause(&mut builder, endpoint_ids);
|
let mut where_clause = WhereClause::new();
|
||||||
|
push_in(&mut builder, &mut where_clause, "endpoint_id", endpoint_ids);
|
||||||
builder
|
builder
|
||||||
.push(" AND created_at >= ")
|
.push(" AND created_at >= ")
|
||||||
.push_bind(unix_secs_to_ms_i64(since_unix_secs)?)
|
.push_bind(unix_secs_to_ms_i64(since_unix_secs)?)
|
||||||
@@ -193,7 +196,8 @@ impl RequestCandidateReadRepository for SqliteRequestCandidateRepository {
|
|||||||
let since_ms = unix_secs_to_ms_i64(since_unix_secs)?;
|
let since_ms = unix_secs_to_ms_i64(since_unix_secs)?;
|
||||||
let until_ms = unix_secs_to_ms_i64(until_unix_secs)?;
|
let until_ms = unix_secs_to_ms_i64(until_unix_secs)?;
|
||||||
let mut builder = QueryBuilder::<Sqlite>::new(CANDIDATE_COLUMNS);
|
let mut builder = QueryBuilder::<Sqlite>::new(CANDIDATE_COLUMNS);
|
||||||
push_endpoint_in_clause(&mut builder, endpoint_ids);
|
let mut where_clause = WhereClause::new();
|
||||||
|
push_in(&mut builder, &mut where_clause, "endpoint_id", endpoint_ids);
|
||||||
builder
|
builder
|
||||||
.push(" AND created_at >= ")
|
.push(" AND created_at >= ")
|
||||||
.push_bind(since_ms)
|
.push_bind(since_ms)
|
||||||
@@ -336,20 +340,6 @@ ON CONFLICT(request_id, candidate_index, retry_index) DO UPDATE SET
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push_endpoint_in_clause<'args>(
|
|
||||||
builder: &mut QueryBuilder<'args, Sqlite>,
|
|
||||||
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(
|
fn merge_candidate(
|
||||||
candidate: UpsertRequestCandidateRecord,
|
candidate: UpsertRequestCandidateRecord,
|
||||||
existing: Option<StoredRequestCandidate>,
|
existing: Option<StoredRequestCandidate>,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use super::types::{
|
|||||||
use crate::driver::mysql::MysqlPool;
|
use crate::driver::mysql::MysqlPool;
|
||||||
use crate::error::SqlResultExt;
|
use crate::error::SqlResultExt;
|
||||||
use crate::DataLayerError;
|
use crate::DataLayerError;
|
||||||
|
use aether_data_query::{push_ci_contains_any, push_limit_offset, SqlDialect, WhereClause};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct MysqlGeminiFileMappingRepository {
|
pub struct MysqlGeminiFileMappingRepository {
|
||||||
@@ -242,8 +243,9 @@ LIMIT 1
|
|||||||
|
|
||||||
fn build_list_count_query(query: &GeminiFileMappingListQuery) -> QueryBuilder<'_, MySql> {
|
fn build_list_count_query(query: &GeminiFileMappingListQuery) -> QueryBuilder<'_, MySql> {
|
||||||
let mut builder =
|
let mut builder =
|
||||||
QueryBuilder::<MySql>::new("SELECT COUNT(*) AS total FROM gemini_file_mappings WHERE 1=1");
|
QueryBuilder::<MySql>::new("SELECT COUNT(*) AS total FROM gemini_file_mappings");
|
||||||
apply_list_filters(&mut builder, query);
|
let mut where_clause = WhereClause::new();
|
||||||
|
apply_list_filters(&mut builder, &mut where_clause, query);
|
||||||
builder
|
builder
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,20 +263,27 @@ SELECT
|
|||||||
created_at AS created_at_unix_ms,
|
created_at AS created_at_unix_ms,
|
||||||
expires_at AS expires_at_unix_secs
|
expires_at AS expires_at_unix_secs
|
||||||
FROM gemini_file_mappings
|
FROM gemini_file_mappings
|
||||||
WHERE 1=1
|
|
||||||
"#,
|
"#,
|
||||||
);
|
);
|
||||||
apply_list_filters(&mut builder, query);
|
let mut where_clause = WhereClause::new();
|
||||||
builder.push(" ORDER BY created_at DESC, file_name ASC LIMIT ");
|
apply_list_filters(&mut builder, &mut where_clause, query);
|
||||||
builder.push_bind(i64::try_from(query.limit).unwrap_or(i64::MAX));
|
builder.push(" ORDER BY created_at DESC, file_name ASC");
|
||||||
builder.push(" OFFSET ");
|
push_limit_offset(
|
||||||
builder.push_bind(i64::try_from(query.offset).unwrap_or(i64::MAX));
|
&mut builder,
|
||||||
|
i64::try_from(query.limit).unwrap_or(i64::MAX),
|
||||||
|
i64::try_from(query.offset).unwrap_or(i64::MAX),
|
||||||
|
);
|
||||||
builder
|
builder
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_list_filters(builder: &mut QueryBuilder<'_, MySql>, query: &GeminiFileMappingListQuery) {
|
fn apply_list_filters(
|
||||||
|
builder: &mut QueryBuilder<'_, MySql>,
|
||||||
|
where_clause: &mut WhereClause,
|
||||||
|
query: &GeminiFileMappingListQuery,
|
||||||
|
) {
|
||||||
if !query.include_expired {
|
if !query.include_expired {
|
||||||
builder.push(" AND expires_at > ");
|
where_clause.push_next(builder);
|
||||||
|
builder.push("expires_at > ");
|
||||||
builder.push_bind(query.now_unix_secs as i64);
|
builder.push_bind(query.now_unix_secs as i64);
|
||||||
}
|
}
|
||||||
if let Some(search) = query
|
if let Some(search) = query
|
||||||
@@ -283,12 +292,13 @@ fn apply_list_filters(builder: &mut QueryBuilder<'_, MySql>, query: &GeminiFileM
|
|||||||
.map(str::trim)
|
.map(str::trim)
|
||||||
.filter(|value| !value.is_empty())
|
.filter(|value| !value.is_empty())
|
||||||
{
|
{
|
||||||
let pattern = format!("%{}%", search.to_ascii_lowercase());
|
push_ci_contains_any(
|
||||||
builder.push(" AND (LOWER(file_name) LIKE ");
|
builder,
|
||||||
builder.push_bind(pattern.clone());
|
where_clause,
|
||||||
builder.push(" OR LOWER(COALESCE(display_name, '')) LIKE ");
|
SqlDialect::Mysql,
|
||||||
builder.push_bind(pattern);
|
&["file_name", "COALESCE(display_name, '')"],
|
||||||
builder.push(")");
|
search,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use super::types::{
|
|||||||
StoredGeminiFileMappingListPage, UpsertGeminiFileMappingRecord,
|
StoredGeminiFileMappingListPage, UpsertGeminiFileMappingRecord,
|
||||||
};
|
};
|
||||||
use crate::{error::SqlxResultExt, DataLayerError};
|
use crate::{error::SqlxResultExt, DataLayerError};
|
||||||
|
use aether_data_query::{push_ci_contains_any, push_limit_offset, SqlDialect, WhereClause};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct SqlxGeminiFileMappingRepository {
|
pub struct SqlxGeminiFileMappingRepository {
|
||||||
@@ -283,10 +284,10 @@ WHERE expires_at <= TO_TIMESTAMP($1::double precision)
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn build_list_count_query(query: &GeminiFileMappingListQuery) -> QueryBuilder<'_, Postgres> {
|
fn build_list_count_query(query: &GeminiFileMappingListQuery) -> QueryBuilder<'_, Postgres> {
|
||||||
let mut builder = QueryBuilder::<Postgres>::new(
|
let mut builder =
|
||||||
"SELECT COUNT(*)::bigint AS total FROM gemini_file_mappings WHERE 1=1",
|
QueryBuilder::<Postgres>::new("SELECT COUNT(*)::bigint AS total FROM gemini_file_mappings");
|
||||||
);
|
let mut where_clause = WhereClause::new();
|
||||||
apply_list_filters(&mut builder, query);
|
apply_list_filters(&mut builder, &mut where_clause, query);
|
||||||
builder
|
builder
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,23 +305,27 @@ SELECT
|
|||||||
EXTRACT(EPOCH FROM created_at)::bigint AS created_at_unix_ms,
|
EXTRACT(EPOCH FROM created_at)::bigint AS created_at_unix_ms,
|
||||||
EXTRACT(EPOCH FROM expires_at)::bigint AS expires_at_unix_secs
|
EXTRACT(EPOCH FROM expires_at)::bigint AS expires_at_unix_secs
|
||||||
FROM gemini_file_mappings
|
FROM gemini_file_mappings
|
||||||
WHERE 1=1
|
|
||||||
"#,
|
"#,
|
||||||
);
|
);
|
||||||
apply_list_filters(&mut builder, query);
|
let mut where_clause = WhereClause::new();
|
||||||
builder.push(" ORDER BY created_at DESC, file_name ASC LIMIT ");
|
apply_list_filters(&mut builder, &mut where_clause, query);
|
||||||
builder.push_bind(i64::try_from(query.limit).unwrap_or(i64::MAX));
|
builder.push(" ORDER BY created_at DESC, file_name ASC");
|
||||||
builder.push(" OFFSET ");
|
push_limit_offset(
|
||||||
builder.push_bind(i64::try_from(query.offset).unwrap_or(i64::MAX));
|
&mut builder,
|
||||||
|
i64::try_from(query.limit).unwrap_or(i64::MAX),
|
||||||
|
i64::try_from(query.offset).unwrap_or(i64::MAX),
|
||||||
|
);
|
||||||
builder
|
builder
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_list_filters(
|
fn apply_list_filters(
|
||||||
builder: &mut QueryBuilder<'_, Postgres>,
|
builder: &mut QueryBuilder<'_, Postgres>,
|
||||||
|
where_clause: &mut WhereClause,
|
||||||
query: &GeminiFileMappingListQuery,
|
query: &GeminiFileMappingListQuery,
|
||||||
) {
|
) {
|
||||||
if !query.include_expired {
|
if !query.include_expired {
|
||||||
builder.push(" AND expires_at > TO_TIMESTAMP(");
|
where_clause.push_next(builder);
|
||||||
|
builder.push("expires_at > TO_TIMESTAMP(");
|
||||||
builder.push_bind(query.now_unix_secs as f64);
|
builder.push_bind(query.now_unix_secs as f64);
|
||||||
builder.push("::double precision)");
|
builder.push("::double precision)");
|
||||||
}
|
}
|
||||||
@@ -330,11 +335,12 @@ fn apply_list_filters(
|
|||||||
.map(str::trim)
|
.map(str::trim)
|
||||||
.filter(|value| !value.is_empty())
|
.filter(|value| !value.is_empty())
|
||||||
{
|
{
|
||||||
let pattern = format!("%{search}%");
|
push_ci_contains_any(
|
||||||
builder.push(" AND (file_name ILIKE ");
|
builder,
|
||||||
builder.push_bind(pattern.clone());
|
where_clause,
|
||||||
builder.push(" OR COALESCE(display_name, '') ILIKE ");
|
SqlDialect::Postgres,
|
||||||
builder.push_bind(pattern);
|
&["file_name", "COALESCE(display_name, '')"],
|
||||||
builder.push(")");
|
search,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use super::types::{
|
|||||||
use crate::driver::sqlite::SqlitePool;
|
use crate::driver::sqlite::SqlitePool;
|
||||||
use crate::error::SqlResultExt;
|
use crate::error::SqlResultExt;
|
||||||
use crate::DataLayerError;
|
use crate::DataLayerError;
|
||||||
|
use aether_data_query::{push_ci_contains_any, push_limit_offset, SqlDialect, WhereClause};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct SqliteGeminiFileMappingRepository {
|
pub struct SqliteGeminiFileMappingRepository {
|
||||||
@@ -242,8 +243,9 @@ LIMIT 1
|
|||||||
|
|
||||||
fn build_list_count_query(query: &GeminiFileMappingListQuery) -> QueryBuilder<'_, Sqlite> {
|
fn build_list_count_query(query: &GeminiFileMappingListQuery) -> QueryBuilder<'_, Sqlite> {
|
||||||
let mut builder =
|
let mut builder =
|
||||||
QueryBuilder::<Sqlite>::new("SELECT COUNT(*) AS total FROM gemini_file_mappings WHERE 1=1");
|
QueryBuilder::<Sqlite>::new("SELECT COUNT(*) AS total FROM gemini_file_mappings");
|
||||||
apply_list_filters(&mut builder, query);
|
let mut where_clause = WhereClause::new();
|
||||||
|
apply_list_filters(&mut builder, &mut where_clause, query);
|
||||||
builder
|
builder
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,20 +263,27 @@ SELECT
|
|||||||
created_at AS created_at_unix_ms,
|
created_at AS created_at_unix_ms,
|
||||||
expires_at AS expires_at_unix_secs
|
expires_at AS expires_at_unix_secs
|
||||||
FROM gemini_file_mappings
|
FROM gemini_file_mappings
|
||||||
WHERE 1=1
|
|
||||||
"#,
|
"#,
|
||||||
);
|
);
|
||||||
apply_list_filters(&mut builder, query);
|
let mut where_clause = WhereClause::new();
|
||||||
builder.push(" ORDER BY created_at DESC, file_name ASC LIMIT ");
|
apply_list_filters(&mut builder, &mut where_clause, query);
|
||||||
builder.push_bind(i64::try_from(query.limit).unwrap_or(i64::MAX));
|
builder.push(" ORDER BY created_at DESC, file_name ASC");
|
||||||
builder.push(" OFFSET ");
|
push_limit_offset(
|
||||||
builder.push_bind(i64::try_from(query.offset).unwrap_or(i64::MAX));
|
&mut builder,
|
||||||
|
i64::try_from(query.limit).unwrap_or(i64::MAX),
|
||||||
|
i64::try_from(query.offset).unwrap_or(i64::MAX),
|
||||||
|
);
|
||||||
builder
|
builder
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_list_filters(builder: &mut QueryBuilder<'_, Sqlite>, query: &GeminiFileMappingListQuery) {
|
fn apply_list_filters(
|
||||||
|
builder: &mut QueryBuilder<'_, Sqlite>,
|
||||||
|
where_clause: &mut WhereClause,
|
||||||
|
query: &GeminiFileMappingListQuery,
|
||||||
|
) {
|
||||||
if !query.include_expired {
|
if !query.include_expired {
|
||||||
builder.push(" AND expires_at > ");
|
where_clause.push_next(builder);
|
||||||
|
builder.push("expires_at > ");
|
||||||
builder.push_bind(query.now_unix_secs as i64);
|
builder.push_bind(query.now_unix_secs as i64);
|
||||||
}
|
}
|
||||||
if let Some(search) = query
|
if let Some(search) = query
|
||||||
@@ -283,12 +292,13 @@ fn apply_list_filters(builder: &mut QueryBuilder<'_, Sqlite>, query: &GeminiFile
|
|||||||
.map(str::trim)
|
.map(str::trim)
|
||||||
.filter(|value| !value.is_empty())
|
.filter(|value| !value.is_empty())
|
||||||
{
|
{
|
||||||
let pattern = format!("%{}%", search.to_ascii_lowercase());
|
push_ci_contains_any(
|
||||||
builder.push(" AND (LOWER(file_name) LIKE ");
|
builder,
|
||||||
builder.push_bind(pattern.clone());
|
where_clause,
|
||||||
builder.push(" OR LOWER(COALESCE(display_name, '')) LIKE ");
|
SqlDialect::Sqlite,
|
||||||
builder.push_bind(pattern);
|
&["file_name", "COALESCE(display_name, '')"],
|
||||||
builder.push(")");
|
search,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,50 +1,20 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use sqlx::{sqlite::SqliteRow, Row};
|
use sqlx::{sqlite::SqliteRow, QueryBuilder, Row, Sqlite};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
metadata_supports_embedding, AdminGlobalModelListQuery, AdminProviderModelListQuery,
|
metadata_supports_embedding, AdminGlobalModelListQuery, AdminProviderModelListQuery,
|
||||||
CreateAdminGlobalModelRecord, GlobalModelReadRepository, GlobalModelWriteRepository,
|
CreateAdminGlobalModelRecord, GlobalModelReadRepository, GlobalModelWriteRepository,
|
||||||
InMemoryGlobalModelReadRepository, PublicCatalogModelListQuery, PublicCatalogModelSearchQuery,
|
PublicCatalogModelListQuery, PublicCatalogModelSearchQuery, PublicGlobalModelQuery,
|
||||||
PublicGlobalModelQuery, StoredAdminGlobalModel, StoredAdminGlobalModelPage,
|
StoredAdminGlobalModel, StoredAdminGlobalModelPage, StoredAdminProviderModel,
|
||||||
StoredAdminProviderModel, StoredProviderActiveGlobalModel, StoredProviderModelStats,
|
StoredProviderActiveGlobalModel, StoredProviderModelStats, StoredPublicCatalogModel,
|
||||||
StoredPublicCatalogModel, StoredPublicGlobalModel, StoredPublicGlobalModelPage,
|
StoredPublicGlobalModel, StoredPublicGlobalModelPage, UpdateAdminGlobalModelRecord,
|
||||||
UpdateAdminGlobalModelRecord, UpsertAdminProviderModelRecord,
|
UpsertAdminProviderModelRecord,
|
||||||
};
|
};
|
||||||
use crate::driver::sqlite::{sqlite_optional_real, SqlitePool};
|
use crate::driver::sqlite::{sqlite_optional_real, SqlitePool};
|
||||||
use crate::error::SqlResultExt;
|
use crate::error::SqlResultExt;
|
||||||
use crate::DataLayerError;
|
use crate::DataLayerError;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
const LIST_PUBLIC_GLOBAL_MODELS_PREFIX: &str = r#"
|
||||||
pub struct SqliteGlobalModelReadRepository {
|
|
||||||
pool: SqlitePool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SqliteGlobalModelReadRepository {
|
|
||||||
pub fn new(pool: SqlitePool) -> Self {
|
|
||||||
Self { pool }
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn load_memory(&self) -> Result<InMemoryGlobalModelReadRepository, DataLayerError> {
|
|
||||||
let public_models = self.load_public_global_models().await?;
|
|
||||||
let admin_global_models = self.load_admin_global_models().await?;
|
|
||||||
let admin_provider_models = self.load_admin_provider_models().await?;
|
|
||||||
let public_catalog_models = self.load_public_catalog_models().await?;
|
|
||||||
let provider_model_stats = self.load_provider_model_stats().await?;
|
|
||||||
let active_global_model_refs = self.load_active_global_model_refs().await?;
|
|
||||||
|
|
||||||
Ok(InMemoryGlobalModelReadRepository::seed(public_models)
|
|
||||||
.with_admin_global_models(admin_global_models)
|
|
||||||
.with_admin_provider_models(admin_provider_models)
|
|
||||||
.with_public_catalog_models(public_catalog_models)
|
|
||||||
.with_provider_model_stats(provider_model_stats)
|
|
||||||
.with_active_global_model_refs(active_global_model_refs))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn load_public_global_models(
|
|
||||||
&self,
|
|
||||||
) -> Result<Vec<StoredPublicGlobalModel>, DataLayerError> {
|
|
||||||
let rows = sqlx::query(
|
|
||||||
r#"
|
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
@@ -54,47 +24,73 @@ SELECT
|
|||||||
default_tiered_pricing,
|
default_tiered_pricing,
|
||||||
supported_capabilities,
|
supported_capabilities,
|
||||||
config,
|
config,
|
||||||
usage_count
|
0 AS usage_count
|
||||||
FROM global_models
|
FROM global_models
|
||||||
"#,
|
"#;
|
||||||
)
|
|
||||||
.fetch_all(&self.pool)
|
|
||||||
.await
|
|
||||||
.map_sql_err()?;
|
|
||||||
rows.iter().map(map_public_global_model_row).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn load_admin_global_models(
|
const COUNT_PUBLIC_GLOBAL_MODELS_PREFIX: &str = r#"
|
||||||
&self,
|
SELECT COUNT(id) AS total
|
||||||
) -> Result<Vec<StoredAdminGlobalModel>, DataLayerError> {
|
FROM global_models
|
||||||
let rows = sqlx::query(
|
"#;
|
||||||
r#"
|
|
||||||
|
const LIST_PUBLIC_CATALOG_MODELS_PREFIX: &str = r#"
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
m.id,
|
||||||
name,
|
m.provider_id,
|
||||||
COALESCE(NULLIF(display_name, ''), name) AS display_name,
|
p.name AS provider_name,
|
||||||
is_active,
|
p.is_active AS provider_is_active,
|
||||||
CAST(default_price_per_request AS REAL) AS default_price_per_request,
|
m.provider_model_name,
|
||||||
default_tiered_pricing,
|
COALESCE(gm.name, m.provider_model_name) AS name,
|
||||||
supported_capabilities,
|
COALESCE(NULLIF(gm.display_name, ''), m.provider_model_name) AS display_name,
|
||||||
config,
|
gm.config AS global_model_config,
|
||||||
usage_count,
|
gm.supported_capabilities AS global_model_supported_capabilities,
|
||||||
created_at AS created_at_unix_ms,
|
m.config AS model_config,
|
||||||
updated_at AS updated_at_unix_secs
|
m.tiered_pricing,
|
||||||
FROM global_models
|
gm.default_tiered_pricing,
|
||||||
"#,
|
COALESCE(
|
||||||
)
|
m.supports_vision,
|
||||||
.fetch_all(&self.pool)
|
CASE
|
||||||
.await
|
WHEN json_extract(gm.config, '$.vision') IS NULL THEN NULL
|
||||||
.map_sql_err()?;
|
WHEN LOWER(CAST(json_extract(gm.config, '$.vision') AS TEXT)) IN ('true', '1') THEN 1
|
||||||
rows.iter().map(map_admin_global_model_row).collect()
|
ELSE 0
|
||||||
}
|
END,
|
||||||
|
0
|
||||||
|
) AS supports_vision,
|
||||||
|
COALESCE(
|
||||||
|
m.supports_function_calling,
|
||||||
|
CASE
|
||||||
|
WHEN json_extract(gm.config, '$.function_calling') IS NULL THEN NULL
|
||||||
|
WHEN LOWER(CAST(json_extract(gm.config, '$.function_calling') AS TEXT)) IN ('true', '1') THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END,
|
||||||
|
0
|
||||||
|
) AS supports_function_calling,
|
||||||
|
COALESCE(
|
||||||
|
m.supports_streaming,
|
||||||
|
CASE
|
||||||
|
WHEN json_extract(gm.config, '$.streaming') IS NULL THEN NULL
|
||||||
|
WHEN LOWER(CAST(json_extract(gm.config, '$.streaming') AS TEXT)) IN ('true', '1') THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END,
|
||||||
|
1
|
||||||
|
) AS supports_streaming,
|
||||||
|
m.is_active,
|
||||||
|
gm.is_active AS global_model_is_active
|
||||||
|
FROM models m
|
||||||
|
JOIN providers p ON p.id = m.provider_id
|
||||||
|
LEFT JOIN global_models gm ON gm.id = m.global_model_id
|
||||||
|
"#;
|
||||||
|
|
||||||
async fn load_admin_provider_models(
|
const LIST_PROVIDER_MODEL_STATS_PREFIX: &str = r#"
|
||||||
&self,
|
SELECT
|
||||||
) -> Result<Vec<StoredAdminProviderModel>, DataLayerError> {
|
provider_id,
|
||||||
let rows = sqlx::query(
|
COUNT(id) AS total_models,
|
||||||
r#"
|
COALESCE(SUM(CASE WHEN is_active = 1 THEN 1 ELSE 0 END), 0) AS active_models
|
||||||
|
FROM models
|
||||||
|
WHERE provider_id IN (
|
||||||
|
"#;
|
||||||
|
|
||||||
|
const LIST_ADMIN_PROVIDER_MODELS_PREFIX: &str = r#"
|
||||||
SELECT
|
SELECT
|
||||||
m.id,
|
m.id,
|
||||||
m.provider_id,
|
m.provider_id,
|
||||||
@@ -109,7 +105,7 @@ SELECT
|
|||||||
m.supports_extended_thinking,
|
m.supports_extended_thinking,
|
||||||
m.supports_image_generation,
|
m.supports_image_generation,
|
||||||
m.is_active,
|
m.is_active,
|
||||||
m.is_available,
|
COALESCE(m.is_available, 1) AS is_available,
|
||||||
m.config,
|
m.config,
|
||||||
m.created_at AS created_at_unix_ms,
|
m.created_at AS created_at_unix_ms,
|
||||||
m.updated_at AS updated_at_unix_secs,
|
m.updated_at AS updated_at_unix_secs,
|
||||||
@@ -121,85 +117,61 @@ SELECT
|
|||||||
gm.config AS global_model_config
|
gm.config AS global_model_config
|
||||||
FROM models m
|
FROM models m
|
||||||
LEFT JOIN global_models gm ON gm.id = m.global_model_id
|
LEFT JOIN global_models gm ON gm.id = m.global_model_id
|
||||||
WHERE m.global_model_id IS NOT NULL
|
"#;
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.fetch_all(&self.pool)
|
|
||||||
.await
|
|
||||||
.map_sql_err()?;
|
|
||||||
rows.iter().map(map_admin_provider_model_row).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn load_public_catalog_models(
|
const LIST_ADMIN_GLOBAL_MODELS_PREFIX: &str = r#"
|
||||||
&self,
|
|
||||||
) -> Result<Vec<StoredPublicCatalogModel>, DataLayerError> {
|
|
||||||
let rows = sqlx::query(
|
|
||||||
r#"
|
|
||||||
SELECT
|
SELECT
|
||||||
m.id,
|
gm.id,
|
||||||
m.provider_id,
|
gm.name,
|
||||||
p.name AS provider_name,
|
COALESCE(NULLIF(gm.display_name, ''), gm.name) AS display_name,
|
||||||
p.is_active AS provider_is_active,
|
gm.is_active,
|
||||||
m.provider_model_name,
|
CAST(gm.default_price_per_request AS REAL) AS default_price_per_request,
|
||||||
COALESCE(gm.name, m.provider_model_name) AS name,
|
|
||||||
COALESCE(NULLIF(gm.display_name, ''), m.provider_model_name) AS display_name,
|
|
||||||
gm.config AS global_model_config,
|
|
||||||
gm.supported_capabilities AS global_model_supported_capabilities,
|
|
||||||
m.config AS model_config,
|
|
||||||
m.tiered_pricing,
|
|
||||||
gm.default_tiered_pricing,
|
gm.default_tiered_pricing,
|
||||||
m.supports_vision,
|
gm.supported_capabilities,
|
||||||
m.supports_function_calling,
|
gm.config,
|
||||||
m.supports_streaming,
|
COALESCE(gm_stats.provider_count, 0) AS provider_count,
|
||||||
m.is_active,
|
COALESCE(gm_stats.active_provider_count, 0) AS active_provider_count,
|
||||||
gm.is_active AS global_model_is_active
|
COALESCE(gm.usage_count, 0) AS usage_count,
|
||||||
|
gm.created_at AS created_at_unix_ms,
|
||||||
|
gm.updated_at AS updated_at_unix_secs
|
||||||
|
FROM global_models gm
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT
|
||||||
|
m.global_model_id,
|
||||||
|
COUNT(DISTINCT m.provider_id) AS provider_count,
|
||||||
|
COUNT(
|
||||||
|
DISTINCT CASE
|
||||||
|
WHEN m.is_active = 1 AND COALESCE(m.is_available, 1) = 1 AND p.is_active = 1 THEN m.provider_id
|
||||||
|
ELSE NULL
|
||||||
|
END
|
||||||
|
) AS active_provider_count
|
||||||
FROM models m
|
FROM models m
|
||||||
JOIN providers p ON p.id = m.provider_id
|
JOIN providers p ON p.id = m.provider_id
|
||||||
LEFT JOIN global_models gm ON gm.id = m.global_model_id
|
GROUP BY m.global_model_id
|
||||||
"#,
|
) gm_stats ON gm_stats.global_model_id = gm.id
|
||||||
)
|
"#;
|
||||||
.fetch_all(&self.pool)
|
|
||||||
.await
|
|
||||||
.map_sql_err()?;
|
|
||||||
rows.iter().map(map_public_catalog_model_row).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn load_provider_model_stats(
|
const COUNT_ADMIN_GLOBAL_MODELS_PREFIX: &str = r#"
|
||||||
&self,
|
SELECT COUNT(id) AS total
|
||||||
) -> Result<Vec<StoredProviderModelStats>, DataLayerError> {
|
FROM global_models gm
|
||||||
let rows = sqlx::query(
|
"#;
|
||||||
r#"
|
|
||||||
SELECT
|
const LIST_ACTIVE_GLOBAL_MODEL_IDS_BY_PROVIDER_IDS_PREFIX: &str = r#"
|
||||||
|
SELECT DISTINCT
|
||||||
provider_id,
|
provider_id,
|
||||||
COUNT(id) AS total_models,
|
global_model_id
|
||||||
SUM(CASE WHEN is_active = 1 THEN 1 ELSE 0 END) AS active_models
|
|
||||||
FROM models
|
FROM models
|
||||||
GROUP BY provider_id
|
WHERE provider_id IN (
|
||||||
ORDER BY provider_id ASC
|
"#;
|
||||||
"#,
|
|
||||||
)
|
#[derive(Debug, Clone)]
|
||||||
.fetch_all(&self.pool)
|
pub struct SqliteGlobalModelReadRepository {
|
||||||
.await
|
pool: SqlitePool,
|
||||||
.map_sql_err()?;
|
|
||||||
rows.iter().map(map_provider_model_stats_row).collect()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn load_active_global_model_refs(
|
impl SqliteGlobalModelReadRepository {
|
||||||
&self,
|
pub fn new(pool: SqlitePool) -> Self {
|
||||||
) -> Result<Vec<StoredProviderActiveGlobalModel>, DataLayerError> {
|
Self { pool }
|
||||||
let rows = sqlx::query(
|
|
||||||
r#"
|
|
||||||
SELECT DISTINCT provider_id, global_model_id
|
|
||||||
FROM models
|
|
||||||
WHERE is_active = 1
|
|
||||||
AND global_model_id IS NOT NULL
|
|
||||||
ORDER BY provider_id ASC, global_model_id ASC
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.fetch_all(&self.pool)
|
|
||||||
.await
|
|
||||||
.map_sql_err()?;
|
|
||||||
rows.iter().map(map_active_global_model_row).collect()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create_admin_provider_model(
|
pub async fn create_admin_provider_model(
|
||||||
@@ -480,67 +452,172 @@ impl GlobalModelReadRepository for SqliteGlobalModelReadRepository {
|
|||||||
&self,
|
&self,
|
||||||
query: &PublicGlobalModelQuery,
|
query: &PublicGlobalModelQuery,
|
||||||
) -> Result<StoredPublicGlobalModelPage, DataLayerError> {
|
) -> Result<StoredPublicGlobalModelPage, DataLayerError> {
|
||||||
self.load_memory().await?.list_public_models(query).await
|
let mut count_builder = QueryBuilder::<Sqlite>::new(COUNT_PUBLIC_GLOBAL_MODELS_PREFIX);
|
||||||
|
apply_public_model_filters(&mut count_builder, query);
|
||||||
|
let count_row = count_builder
|
||||||
|
.build()
|
||||||
|
.fetch_one(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_sql_err()?;
|
||||||
|
let total = count_row
|
||||||
|
.try_get::<i64, _>("total")
|
||||||
|
.map(|value| value.max(0) as usize)
|
||||||
|
.map_sql_err()?;
|
||||||
|
|
||||||
|
let mut list_builder = QueryBuilder::<Sqlite>::new(LIST_PUBLIC_GLOBAL_MODELS_PREFIX);
|
||||||
|
apply_public_model_filters(&mut list_builder, query);
|
||||||
|
list_builder
|
||||||
|
.push(" ORDER BY name ASC LIMIT ")
|
||||||
|
.push_bind(query.limit as i64)
|
||||||
|
.push(" OFFSET ")
|
||||||
|
.push_bind(query.offset as i64);
|
||||||
|
let rows = list_builder
|
||||||
|
.build()
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_sql_err()?;
|
||||||
|
let items = rows
|
||||||
|
.iter()
|
||||||
|
.map(map_public_global_model_row)
|
||||||
|
.collect::<Result<_, _>>()?;
|
||||||
|
|
||||||
|
Ok(StoredPublicGlobalModelPage { items, total })
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_public_model_by_name(
|
async fn get_public_model_by_name(
|
||||||
&self,
|
&self,
|
||||||
model_name: &str,
|
model_name: &str,
|
||||||
) -> Result<Option<StoredPublicGlobalModel>, DataLayerError> {
|
) -> Result<Option<StoredPublicGlobalModel>, DataLayerError> {
|
||||||
self.load_memory()
|
let row = sqlx::query(
|
||||||
.await?
|
r#"
|
||||||
.get_public_model_by_name(model_name)
|
SELECT
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
display_name,
|
||||||
|
is_active,
|
||||||
|
CAST(default_price_per_request AS REAL) AS default_price_per_request,
|
||||||
|
default_tiered_pricing,
|
||||||
|
supported_capabilities,
|
||||||
|
config,
|
||||||
|
0 AS usage_count
|
||||||
|
FROM global_models
|
||||||
|
WHERE name = ? AND is_active = 1
|
||||||
|
LIMIT 1
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(model_name)
|
||||||
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
|
.map_sql_err()?;
|
||||||
|
|
||||||
|
row.as_ref().map(map_public_global_model_row).transpose()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_public_catalog_models(
|
async fn list_public_catalog_models(
|
||||||
&self,
|
&self,
|
||||||
query: &PublicCatalogModelListQuery,
|
query: &PublicCatalogModelListQuery,
|
||||||
) -> Result<Vec<StoredPublicCatalogModel>, DataLayerError> {
|
) -> Result<Vec<StoredPublicCatalogModel>, DataLayerError> {
|
||||||
self.load_memory()
|
let mut builder = QueryBuilder::<Sqlite>::new(LIST_PUBLIC_CATALOG_MODELS_PREFIX);
|
||||||
.await?
|
apply_public_catalog_model_filters(&mut builder, query.provider_id.as_deref(), None);
|
||||||
.list_public_catalog_models(query)
|
builder
|
||||||
.await
|
.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)
|
||||||
|
.push(" OFFSET ")
|
||||||
|
.push_bind(query.offset as i64);
|
||||||
|
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
||||||
|
rows.iter().map(map_public_catalog_model_row).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn search_public_catalog_models(
|
async fn search_public_catalog_models(
|
||||||
&self,
|
&self,
|
||||||
query: &PublicCatalogModelSearchQuery,
|
query: &PublicCatalogModelSearchQuery,
|
||||||
) -> Result<Vec<StoredPublicCatalogModel>, DataLayerError> {
|
) -> Result<Vec<StoredPublicCatalogModel>, DataLayerError> {
|
||||||
self.load_memory()
|
let mut builder = QueryBuilder::<Sqlite>::new(LIST_PUBLIC_CATALOG_MODELS_PREFIX);
|
||||||
.await?
|
apply_public_catalog_model_filters(
|
||||||
.search_public_catalog_models(query)
|
&mut builder,
|
||||||
.await
|
query.provider_id.as_deref(),
|
||||||
|
Some(query.search.as_str()),
|
||||||
|
);
|
||||||
|
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_sql_err()?;
|
||||||
|
rows.iter().map(map_public_catalog_model_row).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_admin_global_models(
|
async fn list_admin_global_models(
|
||||||
&self,
|
&self,
|
||||||
query: &AdminGlobalModelListQuery,
|
query: &AdminGlobalModelListQuery,
|
||||||
) -> Result<StoredAdminGlobalModelPage, DataLayerError> {
|
) -> Result<StoredAdminGlobalModelPage, DataLayerError> {
|
||||||
self.load_memory()
|
let mut count_builder = QueryBuilder::<Sqlite>::new(COUNT_ADMIN_GLOBAL_MODELS_PREFIX);
|
||||||
.await?
|
apply_admin_global_model_filters(&mut count_builder, query);
|
||||||
.list_admin_global_models(query)
|
let count_row = count_builder
|
||||||
|
.build()
|
||||||
|
.fetch_one(&self.pool)
|
||||||
.await
|
.await
|
||||||
|
.map_sql_err()?;
|
||||||
|
let total = count_row
|
||||||
|
.try_get::<i64, _>("total")
|
||||||
|
.map(|value| value.max(0) as usize)
|
||||||
|
.map_sql_err()?;
|
||||||
|
|
||||||
|
let mut list_builder = QueryBuilder::<Sqlite>::new(LIST_ADMIN_GLOBAL_MODELS_PREFIX);
|
||||||
|
apply_admin_global_model_filters(&mut list_builder, query);
|
||||||
|
list_builder
|
||||||
|
.push(" ORDER BY name ASC LIMIT ")
|
||||||
|
.push_bind(query.limit as i64)
|
||||||
|
.push(" OFFSET ")
|
||||||
|
.push_bind(query.offset as i64);
|
||||||
|
let rows = list_builder
|
||||||
|
.build()
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_sql_err()?;
|
||||||
|
let items = rows
|
||||||
|
.iter()
|
||||||
|
.map(map_admin_global_model_row)
|
||||||
|
.collect::<Result<_, _>>()?;
|
||||||
|
Ok(StoredAdminGlobalModelPage { items, total })
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_admin_provider_models(
|
async fn list_admin_provider_models(
|
||||||
&self,
|
&self,
|
||||||
query: &AdminProviderModelListQuery,
|
query: &AdminProviderModelListQuery,
|
||||||
) -> Result<Vec<StoredAdminProviderModel>, DataLayerError> {
|
) -> Result<Vec<StoredAdminProviderModel>, DataLayerError> {
|
||||||
self.load_memory()
|
let mut builder = QueryBuilder::<Sqlite>::new(LIST_ADMIN_PROVIDER_MODELS_PREFIX);
|
||||||
.await?
|
builder
|
||||||
.list_admin_provider_models(query)
|
.push(" WHERE m.provider_id = ")
|
||||||
.await
|
.push_bind(query.provider_id.trim().to_string());
|
||||||
|
if let Some(is_active) = query.is_active {
|
||||||
|
builder.push(" AND m.is_active = ").push_bind(is_active);
|
||||||
|
}
|
||||||
|
builder
|
||||||
|
.push(" ORDER BY m.created_at DESC, m.id ASC LIMIT ")
|
||||||
|
.push_bind(query.limit as i64)
|
||||||
|
.push(" OFFSET ")
|
||||||
|
.push_bind(query.offset as i64);
|
||||||
|
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
||||||
|
rows.iter().map(map_admin_provider_model_row).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_admin_provider_available_source_models(
|
async fn list_admin_provider_available_source_models(
|
||||||
&self,
|
&self,
|
||||||
provider_id: &str,
|
provider_id: &str,
|
||||||
) -> Result<Vec<StoredAdminProviderModel>, DataLayerError> {
|
) -> Result<Vec<StoredAdminProviderModel>, DataLayerError> {
|
||||||
self.load_memory()
|
let rows = sqlx::query(&format!(
|
||||||
.await?
|
r#"
|
||||||
.list_admin_provider_available_source_models(provider_id)
|
{LIST_ADMIN_PROVIDER_MODELS_PREFIX}
|
||||||
|
WHERE m.provider_id = ?
|
||||||
|
AND m.is_active = 1
|
||||||
|
AND gm.is_active = 1
|
||||||
|
ORDER BY gm.name ASC, m.created_at DESC, m.id ASC
|
||||||
|
"#
|
||||||
|
))
|
||||||
|
.bind(provider_id)
|
||||||
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
|
.map_sql_err()?;
|
||||||
|
rows.iter().map(map_admin_provider_model_row).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_admin_provider_model(
|
async fn get_admin_provider_model(
|
||||||
@@ -548,60 +625,111 @@ impl GlobalModelReadRepository for SqliteGlobalModelReadRepository {
|
|||||||
provider_id: &str,
|
provider_id: &str,
|
||||||
model_id: &str,
|
model_id: &str,
|
||||||
) -> Result<Option<StoredAdminProviderModel>, DataLayerError> {
|
) -> Result<Option<StoredAdminProviderModel>, DataLayerError> {
|
||||||
self.load_memory()
|
let row = sqlx::query(&format!(
|
||||||
.await?
|
r#"
|
||||||
.get_admin_provider_model(provider_id, model_id)
|
{LIST_ADMIN_PROVIDER_MODELS_PREFIX}
|
||||||
|
WHERE m.provider_id = ?
|
||||||
|
AND m.id = ?
|
||||||
|
LIMIT 1
|
||||||
|
"#
|
||||||
|
))
|
||||||
|
.bind(provider_id)
|
||||||
|
.bind(model_id)
|
||||||
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
|
.map_sql_err()?;
|
||||||
|
|
||||||
|
row.as_ref().map(map_admin_provider_model_row).transpose()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_admin_global_model_by_id(
|
async fn get_admin_global_model_by_id(
|
||||||
&self,
|
&self,
|
||||||
global_model_id: &str,
|
global_model_id: &str,
|
||||||
) -> Result<Option<StoredAdminGlobalModel>, DataLayerError> {
|
) -> Result<Option<StoredAdminGlobalModel>, DataLayerError> {
|
||||||
self.load_memory()
|
let row = sqlx::query(&format!(
|
||||||
.await?
|
r#"
|
||||||
.get_admin_global_model_by_id(global_model_id)
|
{LIST_ADMIN_GLOBAL_MODELS_PREFIX}
|
||||||
|
WHERE gm.id = ?
|
||||||
|
LIMIT 1
|
||||||
|
"#
|
||||||
|
))
|
||||||
|
.bind(global_model_id)
|
||||||
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
|
.map_sql_err()?;
|
||||||
|
|
||||||
|
row.as_ref().map(map_admin_global_model_row).transpose()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_admin_global_model_by_name(
|
async fn get_admin_global_model_by_name(
|
||||||
&self,
|
&self,
|
||||||
model_name: &str,
|
model_name: &str,
|
||||||
) -> Result<Option<StoredAdminGlobalModel>, DataLayerError> {
|
) -> Result<Option<StoredAdminGlobalModel>, DataLayerError> {
|
||||||
self.load_memory()
|
let row = sqlx::query(&format!(
|
||||||
.await?
|
r#"
|
||||||
.get_admin_global_model_by_name(model_name)
|
{LIST_ADMIN_GLOBAL_MODELS_PREFIX}
|
||||||
|
WHERE gm.name = ?
|
||||||
|
LIMIT 1
|
||||||
|
"#
|
||||||
|
))
|
||||||
|
.bind(model_name)
|
||||||
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
|
.map_sql_err()?;
|
||||||
|
|
||||||
|
row.as_ref().map(map_admin_global_model_row).transpose()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_admin_provider_models_by_global_model_id(
|
async fn list_admin_provider_models_by_global_model_id(
|
||||||
&self,
|
&self,
|
||||||
global_model_id: &str,
|
global_model_id: &str,
|
||||||
) -> Result<Vec<StoredAdminProviderModel>, DataLayerError> {
|
) -> Result<Vec<StoredAdminProviderModel>, DataLayerError> {
|
||||||
self.load_memory()
|
let rows = sqlx::query(&format!(
|
||||||
.await?
|
r#"
|
||||||
.list_admin_provider_models_by_global_model_id(global_model_id)
|
{LIST_ADMIN_PROVIDER_MODELS_PREFIX}
|
||||||
|
WHERE m.global_model_id = ?
|
||||||
|
ORDER BY m.created_at DESC, m.id ASC
|
||||||
|
"#
|
||||||
|
))
|
||||||
|
.bind(global_model_id)
|
||||||
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
|
.map_sql_err()?;
|
||||||
|
rows.iter().map(map_admin_provider_model_row).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_provider_model_stats(
|
async fn list_provider_model_stats(
|
||||||
&self,
|
&self,
|
||||||
provider_ids: &[String],
|
provider_ids: &[String],
|
||||||
) -> Result<Vec<StoredProviderModelStats>, DataLayerError> {
|
) -> Result<Vec<StoredProviderModelStats>, DataLayerError> {
|
||||||
self.load_memory()
|
if provider_ids.is_empty() {
|
||||||
.await?
|
return Ok(Vec::new());
|
||||||
.list_provider_model_stats(provider_ids)
|
}
|
||||||
.await
|
|
||||||
|
let mut builder = build_provider_id_list_query(
|
||||||
|
LIST_PROVIDER_MODEL_STATS_PREFIX,
|
||||||
|
provider_ids,
|
||||||
|
")\nGROUP BY provider_id\nORDER BY provider_id ASC",
|
||||||
|
);
|
||||||
|
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
||||||
|
rows.iter().map(map_provider_model_stats_row).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_active_global_model_ids_by_provider_ids(
|
async fn list_active_global_model_ids_by_provider_ids(
|
||||||
&self,
|
&self,
|
||||||
provider_ids: &[String],
|
provider_ids: &[String],
|
||||||
) -> Result<Vec<StoredProviderActiveGlobalModel>, DataLayerError> {
|
) -> Result<Vec<StoredProviderActiveGlobalModel>, DataLayerError> {
|
||||||
self.load_memory()
|
if provider_ids.is_empty() {
|
||||||
.await?
|
return Ok(Vec::new());
|
||||||
.list_active_global_model_ids_by_provider_ids(provider_ids)
|
}
|
||||||
.await
|
|
||||||
|
let mut builder = build_provider_id_list_query(
|
||||||
|
LIST_ACTIVE_GLOBAL_MODEL_IDS_BY_PROVIDER_IDS_PREFIX,
|
||||||
|
provider_ids,
|
||||||
|
")\nAND is_active = 1\nAND global_model_id IS NOT NULL\nORDER BY provider_id ASC, global_model_id ASC",
|
||||||
|
);
|
||||||
|
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
||||||
|
rows.iter().map(map_active_global_model_row).collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -705,6 +833,100 @@ fn first_tier_price(value: Option<&serde_json::Value>, key: &str) -> Option<f64>
|
|||||||
.and_then(serde_json::Value::as_f64)
|
.and_then(serde_json::Value::as_f64)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn apply_public_model_filters(
|
||||||
|
builder: &mut QueryBuilder<'_, Sqlite>,
|
||||||
|
query: &PublicGlobalModelQuery,
|
||||||
|
) {
|
||||||
|
builder.push(" WHERE ");
|
||||||
|
match query.is_active {
|
||||||
|
Some(is_active) => {
|
||||||
|
builder.push("is_active = ").push_bind(is_active);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
builder.push("is_active = 1");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(search) = query
|
||||||
|
.search
|
||||||
|
.as_deref()
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
{
|
||||||
|
let pattern = format!("%{}%", search.to_ascii_lowercase());
|
||||||
|
builder
|
||||||
|
.push(" AND (LOWER(name) LIKE ")
|
||||||
|
.push_bind(pattern.clone())
|
||||||
|
.push(" OR LOWER(display_name) LIKE ")
|
||||||
|
.push_bind(pattern)
|
||||||
|
.push(")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_admin_global_model_filters(
|
||||||
|
builder: &mut QueryBuilder<'_, Sqlite>,
|
||||||
|
query: &AdminGlobalModelListQuery,
|
||||||
|
) {
|
||||||
|
builder.push(" WHERE 1=1");
|
||||||
|
if let Some(is_active) = query.is_active {
|
||||||
|
builder.push(" AND gm.is_active = ").push_bind(is_active);
|
||||||
|
}
|
||||||
|
if let Some(search) = query
|
||||||
|
.search
|
||||||
|
.as_deref()
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
{
|
||||||
|
let pattern = format!("%{}%", search.to_ascii_lowercase());
|
||||||
|
builder
|
||||||
|
.push(" AND (LOWER(gm.name) LIKE ")
|
||||||
|
.push_bind(pattern.clone())
|
||||||
|
.push(" OR LOWER(gm.display_name) LIKE ")
|
||||||
|
.push_bind(pattern)
|
||||||
|
.push(")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_public_catalog_model_filters(
|
||||||
|
builder: &mut QueryBuilder<'_, Sqlite>,
|
||||||
|
provider_id: Option<&str>,
|
||||||
|
search: Option<&str>,
|
||||||
|
) {
|
||||||
|
builder.push(" WHERE m.is_active = 1 AND COALESCE(m.is_available, 1) = 1 AND p.is_active = 1 AND COALESCE(gm.is_active, 1) = 1");
|
||||||
|
|
||||||
|
if let Some(provider_id) = provider_id.map(str::trim).filter(|value| !value.is_empty()) {
|
||||||
|
builder
|
||||||
|
.push(" AND m.provider_id = ")
|
||||||
|
.push_bind(provider_id.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(search) = search.map(str::trim).filter(|value| !value.is_empty()) {
|
||||||
|
let pattern = format!("%{}%", search.to_ascii_lowercase());
|
||||||
|
builder
|
||||||
|
.push(" AND (LOWER(m.provider_model_name) LIKE ")
|
||||||
|
.push_bind(pattern.clone())
|
||||||
|
.push(" OR LOWER(gm.name) LIKE ")
|
||||||
|
.push_bind(pattern.clone())
|
||||||
|
.push(" OR LOWER(gm.display_name) LIKE ")
|
||||||
|
.push_bind(pattern)
|
||||||
|
.push(")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_provider_id_list_query<'a>(
|
||||||
|
prefix: &'static str,
|
||||||
|
provider_ids: &'a [String],
|
||||||
|
suffix: &'static str,
|
||||||
|
) -> QueryBuilder<'a, Sqlite> {
|
||||||
|
let mut builder = QueryBuilder::<Sqlite>::new(prefix);
|
||||||
|
let mut separated = builder.separated(", ");
|
||||||
|
for provider_id in provider_ids {
|
||||||
|
separated.push_bind(provider_id);
|
||||||
|
}
|
||||||
|
separated.push_unseparated(suffix);
|
||||||
|
builder
|
||||||
|
}
|
||||||
|
|
||||||
fn map_public_global_model_row(row: &SqliteRow) -> Result<StoredPublicGlobalModel, DataLayerError> {
|
fn map_public_global_model_row(row: &SqliteRow) -> Result<StoredPublicGlobalModel, DataLayerError> {
|
||||||
StoredPublicGlobalModel::new(
|
StoredPublicGlobalModel::new(
|
||||||
row.try_get("id").map_sql_err()?,
|
row.try_get("id").map_sql_err()?,
|
||||||
@@ -726,6 +948,16 @@ fn map_public_global_model_row(row: &SqliteRow) -> Result<StoredPublicGlobalMode
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn map_admin_global_model_row(row: &SqliteRow) -> Result<StoredAdminGlobalModel, DataLayerError> {
|
fn map_admin_global_model_row(row: &SqliteRow) -> Result<StoredAdminGlobalModel, DataLayerError> {
|
||||||
|
let provider_count = row
|
||||||
|
.try_get::<i64, _>("provider_count")
|
||||||
|
.map_sql_err()?
|
||||||
|
.max(0) as u64;
|
||||||
|
let active_provider_count = row
|
||||||
|
.try_get::<i64, _>("active_provider_count")
|
||||||
|
.map_sql_err()?
|
||||||
|
.max(0) as u64;
|
||||||
|
let usage_count = row.try_get::<i64, _>("usage_count").map_sql_err()?.max(0) as u64;
|
||||||
|
|
||||||
StoredAdminGlobalModel::new(
|
StoredAdminGlobalModel::new(
|
||||||
row.try_get("id").map_sql_err()?,
|
row.try_get("id").map_sql_err()?,
|
||||||
row.try_get("name").map_sql_err()?,
|
row.try_get("name").map_sql_err()?,
|
||||||
@@ -741,9 +973,9 @@ fn map_admin_global_model_row(row: &SqliteRow) -> Result<StoredAdminGlobalModel,
|
|||||||
"global_models.supported_capabilities",
|
"global_models.supported_capabilities",
|
||||||
)?,
|
)?,
|
||||||
optional_json_from_string(row.try_get("config").map_sql_err()?, "global_models.config")?,
|
optional_json_from_string(row.try_get("config").map_sql_err()?, "global_models.config")?,
|
||||||
0,
|
provider_count,
|
||||||
0,
|
active_provider_count,
|
||||||
row.try_get::<i64, _>("usage_count").map_sql_err()?.max(0) as u64,
|
usage_count,
|
||||||
optional_u64(
|
optional_u64(
|
||||||
row.try_get("created_at_unix_ms").map_sql_err()?,
|
row.try_get("created_at_unix_ms").map_sql_err()?,
|
||||||
"global_models.created_at",
|
"global_models.created_at",
|
||||||
@@ -898,8 +1130,8 @@ mod tests {
|
|||||||
use crate::lifecycle::migrate::run_sqlite_migrations;
|
use crate::lifecycle::migrate::run_sqlite_migrations;
|
||||||
use crate::repository::global_models::{
|
use crate::repository::global_models::{
|
||||||
AdminGlobalModelListQuery, AdminProviderModelListQuery, CreateAdminGlobalModelRecord,
|
AdminGlobalModelListQuery, AdminProviderModelListQuery, CreateAdminGlobalModelRecord,
|
||||||
GlobalModelReadRepository, PublicCatalogModelSearchQuery, PublicGlobalModelQuery,
|
GlobalModelReadRepository, PublicCatalogModelListQuery, PublicCatalogModelSearchQuery,
|
||||||
UpdateAdminGlobalModelRecord, UpsertAdminProviderModelRecord,
|
PublicGlobalModelQuery, UpdateAdminGlobalModelRecord, UpsertAdminProviderModelRecord,
|
||||||
};
|
};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
@@ -939,6 +1171,18 @@ mod tests {
|
|||||||
assert_eq!(catalog.len(), 1);
|
assert_eq!(catalog.len(), 1);
|
||||||
assert_eq!(catalog[0].input_price_per_1m, Some(2.0));
|
assert_eq!(catalog[0].input_price_per_1m, Some(2.0));
|
||||||
|
|
||||||
|
let catalog_list = repository
|
||||||
|
.list_public_catalog_models(&PublicCatalogModelListQuery {
|
||||||
|
provider_id: None,
|
||||||
|
offset: 0,
|
||||||
|
limit: 10,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("catalog list should load");
|
||||||
|
assert_eq!(catalog_list.len(), 2);
|
||||||
|
assert_eq!(catalog_list[0].provider_id, "provider-1");
|
||||||
|
assert_eq!(catalog_list[1].provider_id, "provider-3");
|
||||||
|
|
||||||
let admin_globals = repository
|
let admin_globals = repository
|
||||||
.list_admin_global_models(&AdminGlobalModelListQuery {
|
.list_admin_global_models(&AdminGlobalModelListQuery {
|
||||||
offset: 0,
|
offset: 0,
|
||||||
@@ -949,7 +1193,8 @@ mod tests {
|
|||||||
.await
|
.await
|
||||||
.expect("admin globals should load");
|
.expect("admin globals should load");
|
||||||
assert_eq!(admin_globals.total, 1);
|
assert_eq!(admin_globals.total, 1);
|
||||||
assert_eq!(admin_globals.items[0].provider_count, 1);
|
assert_eq!(admin_globals.items[0].provider_count, 3);
|
||||||
|
assert_eq!(admin_globals.items[0].active_provider_count, 2);
|
||||||
|
|
||||||
let admin_models = repository
|
let admin_models = repository
|
||||||
.list_admin_provider_models(&AdminProviderModelListQuery {
|
.list_admin_provider_models(&AdminProviderModelListQuery {
|
||||||
@@ -1113,6 +1358,18 @@ mod tests {
|
|||||||
seed_provider(pool).await;
|
seed_provider(pool).await;
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
|
INSERT INTO providers (
|
||||||
|
id, name, provider_type, is_active, provider_priority, created_at, updated_at
|
||||||
|
) VALUES
|
||||||
|
('provider-2', 'Inactive Provider', 'custom', 0, 1, 1, 1),
|
||||||
|
('provider-3', 'Alpha Provider', 'custom', 1, 20, 1, 1)
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.execute(pool)
|
||||||
|
.await
|
||||||
|
.expect("extra providers should seed");
|
||||||
|
sqlx::query(
|
||||||
|
r#"
|
||||||
INSERT INTO global_models (
|
INSERT INTO global_models (
|
||||||
id, name, display_name, is_active, default_tiered_pricing,
|
id, name, display_name, is_active, default_tiered_pricing,
|
||||||
supported_capabilities, usage_count, config, created_at, updated_at
|
supported_capabilities, usage_count, config, created_at, updated_at
|
||||||
@@ -1132,10 +1389,20 @@ INSERT INTO models (
|
|||||||
id, provider_id, global_model_id, provider_model_name, provider_model_mappings,
|
id, provider_id, global_model_id, provider_model_name, provider_model_mappings,
|
||||||
supports_vision, supports_function_calling, supports_streaming, is_active,
|
supports_vision, supports_function_calling, supports_streaming, is_active,
|
||||||
is_available, created_at, updated_at
|
is_available, created_at, updated_at
|
||||||
) VALUES (
|
) VALUES
|
||||||
|
(
|
||||||
'model-1', 'provider-1', 'global-1', 'provider-gpt-4.1', '["gpt-4.1"]',
|
'model-1', 'provider-1', 'global-1', 'provider-gpt-4.1', '["gpt-4.1"]',
|
||||||
1, 1, 1, 1, 1, 4, 5
|
1, 1, 1, 1, 1, 4, 5
|
||||||
)
|
)
|
||||||
|
,
|
||||||
|
(
|
||||||
|
'model-2', 'provider-2', 'global-1', 'inactive-provider-gpt-4.1', '["gpt-4.1"]',
|
||||||
|
1, 1, 1, 1, 1, 6, 7
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'model-3', 'provider-3', 'global-1', 'alpha-provider-gpt-4.1', '["gpt-4.1"]',
|
||||||
|
1, 1, 1, 1, 1, 8, 9
|
||||||
|
)
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.execute(pool)
|
.execute(pool)
|
||||||
@@ -1147,9 +1414,9 @@ INSERT INTO models (
|
|||||||
sqlx::query(
|
sqlx::query(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO providers (
|
INSERT INTO providers (
|
||||||
id, name, provider_type, is_active, created_at, updated_at
|
id, name, provider_type, is_active, provider_priority, created_at, updated_at
|
||||||
) VALUES (
|
) VALUES (
|
||||||
'provider-1', 'Provider One', 'custom', 1, 1, 1
|
'provider-1', 'Zulu Provider', 'custom', 1, 10, 1, 1
|
||||||
)
|
)
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use sqlx::{mysql::MySqlRow, Row};
|
use sqlx::{mysql::MySqlRow, MySql, QueryBuilder, Row};
|
||||||
|
|
||||||
use super::types::{
|
use super::types::{
|
||||||
CreateManagementTokenRecord, ManagementTokenListQuery, ManagementTokenReadRepository,
|
CreateManagementTokenRecord, ManagementTokenListQuery, ManagementTokenReadRepository,
|
||||||
@@ -10,6 +10,7 @@ use super::types::{
|
|||||||
use crate::driver::mysql::MysqlPool;
|
use crate::driver::mysql::MysqlPool;
|
||||||
use crate::error::SqlResultExt;
|
use crate::error::SqlResultExt;
|
||||||
use crate::DataLayerError;
|
use crate::DataLayerError;
|
||||||
|
use aether_data_query::{push_eq, push_limit, push_limit_offset, push_optional_eq, WhereClause};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct MysqlManagementTokenRepository {
|
pub struct MysqlManagementTokenRepository {
|
||||||
@@ -25,8 +26,12 @@ impl MysqlManagementTokenRepository {
|
|||||||
&self,
|
&self,
|
||||||
token_id: &str,
|
token_id: &str,
|
||||||
) -> Result<Option<StoredManagementToken>, DataLayerError> {
|
) -> Result<Option<StoredManagementToken>, DataLayerError> {
|
||||||
let row = sqlx::query(TOKEN_BY_ID_SQL)
|
let mut builder = QueryBuilder::<MySql>::new(TOKEN_COLUMNS);
|
||||||
.bind(token_id)
|
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()
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_sql_err()?;
|
.map_sql_err()?;
|
||||||
@@ -34,7 +39,7 @@ impl MysqlManagementTokenRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const TOKEN_BY_ID_SQL: &str = r#"
|
const TOKEN_COLUMNS: &str = r#"
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
user_id,
|
user_id,
|
||||||
@@ -51,11 +56,9 @@ SELECT
|
|||||||
created_at AS created_at_unix_ms,
|
created_at AS created_at_unix_ms,
|
||||||
updated_at AS updated_at_unix_secs
|
updated_at AS updated_at_unix_secs
|
||||||
FROM management_tokens
|
FROM management_tokens
|
||||||
WHERE id = ?
|
|
||||||
LIMIT 1
|
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
const LIST_MANAGEMENT_TOKENS_SQL: &str = r#"
|
const TOKEN_WITH_USER_COLUMNS: &str = r#"
|
||||||
SELECT
|
SELECT
|
||||||
mt.id,
|
mt.id,
|
||||||
mt.user_id,
|
mt.user_id,
|
||||||
@@ -77,69 +80,6 @@ SELECT
|
|||||||
u.role AS user_role
|
u.role AS user_role
|
||||||
FROM management_tokens mt
|
FROM management_tokens mt
|
||||||
JOIN users u ON u.id = mt.user_id
|
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]
|
#[async_trait]
|
||||||
@@ -148,23 +88,27 @@ impl ManagementTokenReadRepository for MysqlManagementTokenRepository {
|
|||||||
&self,
|
&self,
|
||||||
query: &ManagementTokenListQuery,
|
query: &ManagementTokenListQuery,
|
||||||
) -> Result<StoredManagementTokenListPage, DataLayerError> {
|
) -> Result<StoredManagementTokenListPage, DataLayerError> {
|
||||||
let count_row = sqlx::query(COUNT_MANAGEMENT_TOKENS_SQL)
|
let mut count_builder =
|
||||||
.bind(query.user_id.as_deref())
|
QueryBuilder::<MySql>::new("SELECT COUNT(mt.id) AS total FROM management_tokens mt");
|
||||||
.bind(query.user_id.as_deref())
|
let mut count_where = WhereClause::new();
|
||||||
.bind(query.is_active)
|
apply_management_token_filters(&mut count_builder, &mut count_where, query);
|
||||||
.bind(query.is_active)
|
let total = count_builder
|
||||||
|
.build_query_scalar::<i64>()
|
||||||
.fetch_one(&self.pool)
|
.fetch_one(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_sql_err()?;
|
.map_sql_err()?;
|
||||||
let total = count_row.try_get::<i64, _>("total").map_sql_err()?;
|
|
||||||
|
|
||||||
let rows = sqlx::query(LIST_MANAGEMENT_TOKENS_SQL)
|
let mut list_builder = QueryBuilder::<MySql>::new(TOKEN_WITH_USER_COLUMNS);
|
||||||
.bind(query.user_id.as_deref())
|
let mut list_where = WhereClause::new();
|
||||||
.bind(query.user_id.as_deref())
|
apply_management_token_filters(&mut list_builder, &mut list_where, query);
|
||||||
.bind(query.is_active)
|
list_builder.push(" ORDER BY mt.created_at DESC, mt.id DESC");
|
||||||
.bind(query.is_active)
|
push_limit_offset(
|
||||||
.bind(i64::try_from(query.limit).unwrap_or(i64::MAX))
|
&mut list_builder,
|
||||||
.bind(i64::try_from(query.offset).unwrap_or(i64::MAX))
|
i64::try_from(query.limit).unwrap_or(i64::MAX),
|
||||||
|
i64::try_from(query.offset).unwrap_or(i64::MAX),
|
||||||
|
);
|
||||||
|
let rows = list_builder
|
||||||
|
.build()
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_sql_err()?;
|
.map_sql_err()?;
|
||||||
@@ -182,8 +126,17 @@ impl ManagementTokenReadRepository for MysqlManagementTokenRepository {
|
|||||||
&self,
|
&self,
|
||||||
token_id: &str,
|
token_id: &str,
|
||||||
) -> Result<Option<StoredManagementTokenWithUser>, DataLayerError> {
|
) -> Result<Option<StoredManagementTokenWithUser>, DataLayerError> {
|
||||||
let row = sqlx::query(GET_MANAGEMENT_TOKEN_WITH_USER_SQL)
|
let mut builder = QueryBuilder::<MySql>::new(TOKEN_WITH_USER_COLUMNS);
|
||||||
.bind(token_id)
|
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()
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_sql_err()?;
|
.map_sql_err()?;
|
||||||
@@ -194,8 +147,17 @@ impl ManagementTokenReadRepository for MysqlManagementTokenRepository {
|
|||||||
&self,
|
&self,
|
||||||
token_hash: &str,
|
token_hash: &str,
|
||||||
) -> Result<Option<StoredManagementTokenWithUser>, DataLayerError> {
|
) -> Result<Option<StoredManagementTokenWithUser>, DataLayerError> {
|
||||||
let row = sqlx::query(GET_MANAGEMENT_TOKEN_WITH_USER_BY_HASH_SQL)
|
let mut builder = QueryBuilder::<MySql>::new(TOKEN_WITH_USER_COLUMNS);
|
||||||
.bind(token_hash)
|
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()
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_sql_err()?;
|
.map_sql_err()?;
|
||||||
@@ -203,6 +165,15 @@ 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]
|
#[async_trait]
|
||||||
impl ManagementTokenWriteRepository for MysqlManagementTokenRepository {
|
impl ManagementTokenWriteRepository for MysqlManagementTokenRepository {
|
||||||
async fn create_management_token(
|
async fn create_management_token(
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use futures_util::TryStreamExt;
|
use sqlx::{postgres::PgRow, PgPool, Postgres, QueryBuilder, Row};
|
||||||
use sqlx::{postgres::PgRow, PgPool, Row};
|
|
||||||
|
|
||||||
use super::types::{
|
use super::types::{
|
||||||
CreateManagementTokenRecord, ManagementTokenListQuery, ManagementTokenReadRepository,
|
CreateManagementTokenRecord, ManagementTokenListQuery, ManagementTokenReadRepository,
|
||||||
@@ -9,8 +8,9 @@ use super::types::{
|
|||||||
UpdateManagementTokenRecord,
|
UpdateManagementTokenRecord,
|
||||||
};
|
};
|
||||||
use crate::{error::SqlxResultExt, DataLayerError};
|
use crate::{error::SqlxResultExt, DataLayerError};
|
||||||
|
use aether_data_query::{push_eq, push_limit, push_limit_offset, push_optional_eq, WhereClause};
|
||||||
|
|
||||||
const LIST_MANAGEMENT_TOKENS_SQL: &str = r#"
|
const MANAGEMENT_TOKEN_WITH_USER_COLUMNS: &str = r#"
|
||||||
SELECT
|
SELECT
|
||||||
mt.id,
|
mt.id,
|
||||||
mt.user_id,
|
mt.user_id,
|
||||||
@@ -32,70 +32,6 @@ SELECT
|
|||||||
u.role::text AS user_role
|
u.role::text AS user_role
|
||||||
FROM management_tokens mt
|
FROM management_tokens mt
|
||||||
JOIN users u ON u.id = mt.user_id
|
JOIN users u ON u.id = mt.user_id
|
||||||
WHERE ($1::text IS NULL OR mt.user_id = $1)
|
|
||||||
AND ($2::boolean IS NULL OR mt.is_active = $2)
|
|
||||||
ORDER BY mt.created_at DESC, mt.id DESC
|
|
||||||
OFFSET $3
|
|
||||||
LIMIT $4
|
|
||||||
"#;
|
|
||||||
|
|
||||||
const COUNT_MANAGEMENT_TOKENS_SQL: &str = r#"
|
|
||||||
SELECT COUNT(mt.id) AS total
|
|
||||||
FROM management_tokens mt
|
|
||||||
WHERE ($1::text IS NULL OR mt.user_id = $1)
|
|
||||||
AND ($2::boolean IS NULL OR mt.is_active = $2)
|
|
||||||
"#;
|
|
||||||
|
|
||||||
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,
|
|
||||||
EXTRACT(EPOCH FROM mt.expires_at)::bigint AS expires_at_unix_secs,
|
|
||||||
EXTRACT(EPOCH FROM mt.last_used_at)::bigint AS last_used_at_unix_secs,
|
|
||||||
mt.last_used_ip,
|
|
||||||
COALESCE(mt.usage_count, 0) AS usage_count,
|
|
||||||
mt.is_active,
|
|
||||||
EXTRACT(EPOCH FROM mt.created_at)::bigint AS created_at_unix_ms,
|
|
||||||
EXTRACT(EPOCH FROM mt.updated_at)::bigint AS updated_at_unix_secs,
|
|
||||||
u.id AS user_row_id,
|
|
||||||
u.email AS user_email,
|
|
||||||
u.username AS user_username,
|
|
||||||
u.role::text AS user_role
|
|
||||||
FROM management_tokens mt
|
|
||||||
JOIN users u ON u.id = mt.user_id
|
|
||||||
WHERE mt.id = $1
|
|
||||||
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,
|
|
||||||
EXTRACT(EPOCH FROM mt.expires_at)::bigint AS expires_at_unix_secs,
|
|
||||||
EXTRACT(EPOCH FROM mt.last_used_at)::bigint AS last_used_at_unix_secs,
|
|
||||||
mt.last_used_ip,
|
|
||||||
COALESCE(mt.usage_count, 0) AS usage_count,
|
|
||||||
mt.is_active,
|
|
||||||
EXTRACT(EPOCH FROM mt.created_at)::bigint AS created_at_unix_ms,
|
|
||||||
EXTRACT(EPOCH FROM mt.updated_at)::bigint AS updated_at_unix_secs,
|
|
||||||
u.id AS user_row_id,
|
|
||||||
u.email AS user_email,
|
|
||||||
u.username AS user_username,
|
|
||||||
u.role::text AS user_role
|
|
||||||
FROM management_tokens mt
|
|
||||||
JOIN users u ON u.id = mt.user_id
|
|
||||||
WHERE mt.token_hash = $1
|
|
||||||
LIMIT 1
|
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
const DELETE_MANAGEMENT_TOKEN_SQL: &str = r#"
|
const DELETE_MANAGEMENT_TOKEN_SQL: &str = r#"
|
||||||
@@ -362,24 +298,34 @@ impl ManagementTokenReadRepository for SqlxManagementTokenRepository {
|
|||||||
&self,
|
&self,
|
||||||
query: &ManagementTokenListQuery,
|
query: &ManagementTokenListQuery,
|
||||||
) -> Result<StoredManagementTokenListPage, DataLayerError> {
|
) -> Result<StoredManagementTokenListPage, DataLayerError> {
|
||||||
let count_row = sqlx::query(COUNT_MANAGEMENT_TOKENS_SQL)
|
let mut count_builder =
|
||||||
.bind(query.user_id.as_deref())
|
QueryBuilder::<Postgres>::new("SELECT COUNT(mt.id) AS total FROM management_tokens mt");
|
||||||
.bind(query.is_active)
|
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>()
|
||||||
.fetch_one(&self.pool)
|
.fetch_one(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_postgres_err()?;
|
.map_postgres_err()?;
|
||||||
let total = count_row.try_get::<i64, _>("total").map_postgres_err()?;
|
|
||||||
|
|
||||||
let mut rows = sqlx::query(LIST_MANAGEMENT_TOKENS_SQL)
|
let mut list_builder = QueryBuilder::<Postgres>::new(MANAGEMENT_TOKEN_WITH_USER_COLUMNS);
|
||||||
.bind(query.user_id.as_deref())
|
let mut list_where = WhereClause::new();
|
||||||
.bind(query.is_active)
|
apply_management_token_filters(&mut list_builder, &mut list_where, query);
|
||||||
.bind(i64::try_from(query.offset).unwrap_or(i64::MAX))
|
list_builder.push(" ORDER BY mt.created_at DESC, mt.id DESC");
|
||||||
.bind(i64::try_from(query.limit).unwrap_or(i64::MAX))
|
push_limit_offset(
|
||||||
.fetch(&self.pool);
|
&mut list_builder,
|
||||||
let mut items = Vec::new();
|
i64::try_from(query.limit).unwrap_or(i64::MAX),
|
||||||
while let Some(row) = rows.try_next().await.map_postgres_err()? {
|
i64::try_from(query.offset).unwrap_or(i64::MAX),
|
||||||
items.push(map_token_with_user_row(&row)?);
|
);
|
||||||
}
|
let rows = list_builder
|
||||||
|
.build()
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_postgres_err()?;
|
||||||
|
let items = rows
|
||||||
|
.iter()
|
||||||
|
.map(map_token_with_user_row)
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
|
||||||
Ok(StoredManagementTokenListPage {
|
Ok(StoredManagementTokenListPage {
|
||||||
items,
|
items,
|
||||||
@@ -391,8 +337,17 @@ impl ManagementTokenReadRepository for SqlxManagementTokenRepository {
|
|||||||
&self,
|
&self,
|
||||||
token_id: &str,
|
token_id: &str,
|
||||||
) -> Result<Option<StoredManagementTokenWithUser>, DataLayerError> {
|
) -> Result<Option<StoredManagementTokenWithUser>, DataLayerError> {
|
||||||
let row = sqlx::query(GET_MANAGEMENT_TOKEN_WITH_USER_SQL)
|
let mut builder = QueryBuilder::<Postgres>::new(MANAGEMENT_TOKEN_WITH_USER_COLUMNS);
|
||||||
.bind(token_id)
|
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()
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_postgres_err()?;
|
.map_postgres_err()?;
|
||||||
@@ -403,8 +358,17 @@ impl ManagementTokenReadRepository for SqlxManagementTokenRepository {
|
|||||||
&self,
|
&self,
|
||||||
token_hash: &str,
|
token_hash: &str,
|
||||||
) -> Result<Option<StoredManagementTokenWithUser>, DataLayerError> {
|
) -> Result<Option<StoredManagementTokenWithUser>, DataLayerError> {
|
||||||
let row = sqlx::query(GET_MANAGEMENT_TOKEN_WITH_USER_BY_HASH_SQL)
|
let mut builder = QueryBuilder::<Postgres>::new(MANAGEMENT_TOKEN_WITH_USER_COLUMNS);
|
||||||
.bind(token_hash)
|
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()
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_postgres_err()?;
|
.map_postgres_err()?;
|
||||||
@@ -412,6 +376,15 @@ impl ManagementTokenReadRepository for SqlxManagementTokenRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn apply_management_token_filters<'a>(
|
||||||
|
builder: &mut QueryBuilder<'a, Postgres>,
|
||||||
|
where_clause: &mut WhereClause,
|
||||||
|
query: &'a 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]
|
#[async_trait]
|
||||||
impl ManagementTokenWriteRepository for SqlxManagementTokenRepository {
|
impl ManagementTokenWriteRepository for SqlxManagementTokenRepository {
|
||||||
async fn create_management_token(
|
async fn create_management_token(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use sqlx::{sqlite::SqliteRow, Row, SqlitePool};
|
use sqlx::{sqlite::SqliteRow, QueryBuilder, Row, Sqlite, SqlitePool};
|
||||||
|
|
||||||
use super::types::{
|
use super::types::{
|
||||||
CreateManagementTokenRecord, ManagementTokenListQuery, ManagementTokenReadRepository,
|
CreateManagementTokenRecord, ManagementTokenListQuery, ManagementTokenReadRepository,
|
||||||
@@ -9,6 +9,7 @@ use super::types::{
|
|||||||
};
|
};
|
||||||
use crate::error::SqlResultExt;
|
use crate::error::SqlResultExt;
|
||||||
use crate::DataLayerError;
|
use crate::DataLayerError;
|
||||||
|
use aether_data_query::{push_eq, push_limit, push_limit_offset, push_optional_eq, WhereClause};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct SqliteManagementTokenRepository {
|
pub struct SqliteManagementTokenRepository {
|
||||||
@@ -24,8 +25,12 @@ impl SqliteManagementTokenRepository {
|
|||||||
&self,
|
&self,
|
||||||
token_id: &str,
|
token_id: &str,
|
||||||
) -> Result<Option<StoredManagementToken>, DataLayerError> {
|
) -> Result<Option<StoredManagementToken>, DataLayerError> {
|
||||||
let row = sqlx::query(TOKEN_BY_ID_SQL)
|
let mut builder = QueryBuilder::<Sqlite>::new(TOKEN_COLUMNS);
|
||||||
.bind(token_id)
|
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()
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_sql_err()?;
|
.map_sql_err()?;
|
||||||
@@ -33,7 +38,7 @@ impl SqliteManagementTokenRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const TOKEN_BY_ID_SQL: &str = r#"
|
const TOKEN_COLUMNS: &str = r#"
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
user_id,
|
user_id,
|
||||||
@@ -50,11 +55,9 @@ SELECT
|
|||||||
created_at AS created_at_unix_ms,
|
created_at AS created_at_unix_ms,
|
||||||
updated_at AS updated_at_unix_secs
|
updated_at AS updated_at_unix_secs
|
||||||
FROM management_tokens
|
FROM management_tokens
|
||||||
WHERE id = ?
|
|
||||||
LIMIT 1
|
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
const LIST_MANAGEMENT_TOKENS_SQL: &str = r#"
|
const TOKEN_WITH_USER_COLUMNS: &str = r#"
|
||||||
SELECT
|
SELECT
|
||||||
mt.id,
|
mt.id,
|
||||||
mt.user_id,
|
mt.user_id,
|
||||||
@@ -76,69 +79,6 @@ SELECT
|
|||||||
u.role AS user_role
|
u.role AS user_role
|
||||||
FROM management_tokens mt
|
FROM management_tokens mt
|
||||||
JOIN users u ON u.id = mt.user_id
|
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]
|
#[async_trait]
|
||||||
@@ -147,23 +87,27 @@ impl ManagementTokenReadRepository for SqliteManagementTokenRepository {
|
|||||||
&self,
|
&self,
|
||||||
query: &ManagementTokenListQuery,
|
query: &ManagementTokenListQuery,
|
||||||
) -> Result<StoredManagementTokenListPage, DataLayerError> {
|
) -> Result<StoredManagementTokenListPage, DataLayerError> {
|
||||||
let count_row = sqlx::query(COUNT_MANAGEMENT_TOKENS_SQL)
|
let mut count_builder =
|
||||||
.bind(query.user_id.as_deref())
|
QueryBuilder::<Sqlite>::new("SELECT COUNT(mt.id) AS total FROM management_tokens mt");
|
||||||
.bind(query.user_id.as_deref())
|
let mut count_where = WhereClause::new();
|
||||||
.bind(query.is_active)
|
apply_management_token_filters(&mut count_builder, &mut count_where, query);
|
||||||
.bind(query.is_active)
|
let total = count_builder
|
||||||
|
.build_query_scalar::<i64>()
|
||||||
.fetch_one(&self.pool)
|
.fetch_one(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_sql_err()?;
|
.map_sql_err()?;
|
||||||
let total = count_row.try_get::<i64, _>("total").map_sql_err()?;
|
|
||||||
|
|
||||||
let rows = sqlx::query(LIST_MANAGEMENT_TOKENS_SQL)
|
let mut list_builder = QueryBuilder::<Sqlite>::new(TOKEN_WITH_USER_COLUMNS);
|
||||||
.bind(query.user_id.as_deref())
|
let mut list_where = WhereClause::new();
|
||||||
.bind(query.user_id.as_deref())
|
apply_management_token_filters(&mut list_builder, &mut list_where, query);
|
||||||
.bind(query.is_active)
|
list_builder.push(" ORDER BY mt.created_at DESC, mt.id DESC");
|
||||||
.bind(query.is_active)
|
push_limit_offset(
|
||||||
.bind(i64::try_from(query.limit).unwrap_or(i64::MAX))
|
&mut list_builder,
|
||||||
.bind(i64::try_from(query.offset).unwrap_or(i64::MAX))
|
i64::try_from(query.limit).unwrap_or(i64::MAX),
|
||||||
|
i64::try_from(query.offset).unwrap_or(i64::MAX),
|
||||||
|
);
|
||||||
|
let rows = list_builder
|
||||||
|
.build()
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_sql_err()?;
|
.map_sql_err()?;
|
||||||
@@ -181,8 +125,17 @@ impl ManagementTokenReadRepository for SqliteManagementTokenRepository {
|
|||||||
&self,
|
&self,
|
||||||
token_id: &str,
|
token_id: &str,
|
||||||
) -> Result<Option<StoredManagementTokenWithUser>, DataLayerError> {
|
) -> Result<Option<StoredManagementTokenWithUser>, DataLayerError> {
|
||||||
let row = sqlx::query(GET_MANAGEMENT_TOKEN_WITH_USER_SQL)
|
let mut builder = QueryBuilder::<Sqlite>::new(TOKEN_WITH_USER_COLUMNS);
|
||||||
.bind(token_id)
|
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()
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_sql_err()?;
|
.map_sql_err()?;
|
||||||
@@ -193,8 +146,17 @@ impl ManagementTokenReadRepository for SqliteManagementTokenRepository {
|
|||||||
&self,
|
&self,
|
||||||
token_hash: &str,
|
token_hash: &str,
|
||||||
) -> Result<Option<StoredManagementTokenWithUser>, DataLayerError> {
|
) -> Result<Option<StoredManagementTokenWithUser>, DataLayerError> {
|
||||||
let row = sqlx::query(GET_MANAGEMENT_TOKEN_WITH_USER_BY_HASH_SQL)
|
let mut builder = QueryBuilder::<Sqlite>::new(TOKEN_WITH_USER_COLUMNS);
|
||||||
.bind(token_hash)
|
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()
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_sql_err()?;
|
.map_sql_err()?;
|
||||||
@@ -202,6 +164,15 @@ impl ManagementTokenReadRepository for SqliteManagementTokenRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn apply_management_token_filters(
|
||||||
|
builder: &mut QueryBuilder<'_, Sqlite>,
|
||||||
|
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]
|
#[async_trait]
|
||||||
impl ManagementTokenWriteRepository for SqliteManagementTokenRepository {
|
impl ManagementTokenWriteRepository for SqliteManagementTokenRepository {
|
||||||
async fn create_management_token(
|
async fn create_management_token(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use sqlx::{mysql::MySqlRow, Row};
|
use sqlx::{mysql::MySqlRow, MySql, QueryBuilder, Row};
|
||||||
|
|
||||||
use super::types::{
|
use super::types::{
|
||||||
OAuthProviderReadRepository, OAuthProviderWriteRepository, StoredOAuthProviderConfig,
|
OAuthProviderReadRepository, OAuthProviderWriteRepository, StoredOAuthProviderConfig,
|
||||||
@@ -8,6 +8,7 @@ use super::types::{
|
|||||||
use crate::driver::mysql::MysqlPool;
|
use crate::driver::mysql::MysqlPool;
|
||||||
use crate::error::SqlResultExt;
|
use crate::error::SqlResultExt;
|
||||||
use crate::DataLayerError;
|
use crate::DataLayerError;
|
||||||
|
use aether_data_query::{push_eq, push_limit, WhereClause};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct MysqlOAuthProviderRepository {
|
pub struct MysqlOAuthProviderRepository {
|
||||||
@@ -23,8 +24,17 @@ impl MysqlOAuthProviderRepository {
|
|||||||
&self,
|
&self,
|
||||||
provider_type: &str,
|
provider_type: &str,
|
||||||
) -> Result<Option<StoredOAuthProviderConfig>, DataLayerError> {
|
) -> Result<Option<StoredOAuthProviderConfig>, DataLayerError> {
|
||||||
let row = sqlx::query(GET_OAUTH_PROVIDER_CONFIG_SQL)
|
let mut builder = QueryBuilder::<MySql>::new(OAUTH_PROVIDER_COLUMNS);
|
||||||
.bind(provider_type)
|
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()
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_sql_err()?;
|
.map_sql_err()?;
|
||||||
@@ -32,7 +42,7 @@ impl MysqlOAuthProviderRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const LIST_OAUTH_PROVIDER_CONFIGS_SQL: &str = r#"
|
const OAUTH_PROVIDER_COLUMNS: &str = r#"
|
||||||
SELECT
|
SELECT
|
||||||
provider_type,
|
provider_type,
|
||||||
display_name,
|
display_name,
|
||||||
@@ -50,29 +60,6 @@ SELECT
|
|||||||
created_at AS created_at_unix_ms,
|
created_at AS created_at_unix_ms,
|
||||||
updated_at AS updated_at_unix_secs
|
updated_at AS updated_at_unix_secs
|
||||||
FROM oauth_providers
|
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#"
|
const COUNT_LOCKED_USERS_IF_PROVIDER_DISABLED_SQL: &str = r#"
|
||||||
@@ -117,10 +104,9 @@ impl OAuthProviderReadRepository for MysqlOAuthProviderRepository {
|
|||||||
async fn list_oauth_provider_configs(
|
async fn list_oauth_provider_configs(
|
||||||
&self,
|
&self,
|
||||||
) -> Result<Vec<StoredOAuthProviderConfig>, DataLayerError> {
|
) -> Result<Vec<StoredOAuthProviderConfig>, DataLayerError> {
|
||||||
let rows = sqlx::query(LIST_OAUTH_PROVIDER_CONFIGS_SQL)
|
let mut builder = QueryBuilder::<MySql>::new(OAUTH_PROVIDER_COLUMNS);
|
||||||
.fetch_all(&self.pool)
|
builder.push(" ORDER BY provider_type ASC");
|
||||||
.await
|
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
||||||
.map_sql_err()?;
|
|
||||||
rows.iter().map(map_oauth_provider_row).collect()
|
rows.iter().map(map_oauth_provider_row).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use futures_util::TryStreamExt;
|
use sqlx::{postgres::PgRow, PgPool, Postgres, QueryBuilder, Row};
|
||||||
use sqlx::{postgres::PgRow, PgPool, Row};
|
|
||||||
|
|
||||||
use super::types::{
|
use super::types::{
|
||||||
OAuthProviderReadRepository, OAuthProviderWriteRepository, StoredOAuthProviderConfig,
|
OAuthProviderReadRepository, OAuthProviderWriteRepository, StoredOAuthProviderConfig,
|
||||||
UpsertOAuthProviderConfigRecord,
|
UpsertOAuthProviderConfigRecord,
|
||||||
};
|
};
|
||||||
use crate::{error::SqlxResultExt, DataLayerError};
|
use crate::{error::SqlxResultExt, DataLayerError};
|
||||||
|
use aether_data_query::{push_eq, push_limit, WhereClause};
|
||||||
|
|
||||||
const LIST_OAUTH_PROVIDER_CONFIGS_SQL: &str = r#"
|
const OAUTH_PROVIDER_COLUMNS: &str = r#"
|
||||||
SELECT
|
SELECT
|
||||||
provider_type,
|
provider_type,
|
||||||
display_name,
|
display_name,
|
||||||
@@ -26,29 +26,6 @@ SELECT
|
|||||||
EXTRACT(EPOCH FROM created_at)::bigint AS created_at_unix_ms,
|
EXTRACT(EPOCH FROM created_at)::bigint AS created_at_unix_ms,
|
||||||
EXTRACT(EPOCH FROM updated_at)::bigint AS updated_at_unix_secs
|
EXTRACT(EPOCH FROM updated_at)::bigint AS updated_at_unix_secs
|
||||||
FROM oauth_providers
|
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,
|
|
||||||
EXTRACT(EPOCH FROM created_at)::bigint AS created_at_unix_ms,
|
|
||||||
EXTRACT(EPOCH FROM updated_at)::bigint AS updated_at_unix_secs
|
|
||||||
FROM oauth_providers
|
|
||||||
WHERE provider_type = $1
|
|
||||||
LIMIT 1
|
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
const COUNT_LOCKED_USERS_IF_PROVIDER_DISABLED_SQL: &str = r#"
|
const COUNT_LOCKED_USERS_IF_PROVIDER_DISABLED_SQL: &str = r#"
|
||||||
@@ -182,20 +159,31 @@ impl OAuthProviderReadRepository for SqlxOAuthProviderRepository {
|
|||||||
async fn list_oauth_provider_configs(
|
async fn list_oauth_provider_configs(
|
||||||
&self,
|
&self,
|
||||||
) -> Result<Vec<StoredOAuthProviderConfig>, DataLayerError> {
|
) -> Result<Vec<StoredOAuthProviderConfig>, DataLayerError> {
|
||||||
let mut rows = sqlx::query(LIST_OAUTH_PROVIDER_CONFIGS_SQL).fetch(&self.pool);
|
let mut builder = QueryBuilder::<Postgres>::new(OAUTH_PROVIDER_COLUMNS);
|
||||||
let mut items = Vec::new();
|
builder.push(" ORDER BY provider_type ASC");
|
||||||
while let Some(row) = rows.try_next().await.map_postgres_err()? {
|
let rows = builder
|
||||||
items.push(map_oauth_provider_row(&row)?);
|
.build()
|
||||||
}
|
.fetch_all(&self.pool)
|
||||||
Ok(items)
|
.await
|
||||||
|
.map_postgres_err()?;
|
||||||
|
rows.iter().map(map_oauth_provider_row).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_oauth_provider_config(
|
async fn get_oauth_provider_config(
|
||||||
&self,
|
&self,
|
||||||
provider_type: &str,
|
provider_type: &str,
|
||||||
) -> Result<Option<StoredOAuthProviderConfig>, DataLayerError> {
|
) -> Result<Option<StoredOAuthProviderConfig>, DataLayerError> {
|
||||||
let row = sqlx::query(GET_OAUTH_PROVIDER_CONFIG_SQL)
|
let mut builder = QueryBuilder::<Postgres>::new(OAUTH_PROVIDER_COLUMNS);
|
||||||
.bind(provider_type)
|
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()
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_postgres_err()?;
|
.map_postgres_err()?;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use sqlx::{sqlite::SqliteRow, Row};
|
use sqlx::{sqlite::SqliteRow, QueryBuilder, Row, Sqlite};
|
||||||
|
|
||||||
use super::types::{
|
use super::types::{
|
||||||
OAuthProviderReadRepository, OAuthProviderWriteRepository, StoredOAuthProviderConfig,
|
OAuthProviderReadRepository, OAuthProviderWriteRepository, StoredOAuthProviderConfig,
|
||||||
@@ -8,6 +8,7 @@ use super::types::{
|
|||||||
use crate::driver::sqlite::SqlitePool;
|
use crate::driver::sqlite::SqlitePool;
|
||||||
use crate::error::SqlResultExt;
|
use crate::error::SqlResultExt;
|
||||||
use crate::DataLayerError;
|
use crate::DataLayerError;
|
||||||
|
use aether_data_query::{push_eq, push_limit, WhereClause};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct SqliteOAuthProviderRepository {
|
pub struct SqliteOAuthProviderRepository {
|
||||||
@@ -23,8 +24,17 @@ impl SqliteOAuthProviderRepository {
|
|||||||
&self,
|
&self,
|
||||||
provider_type: &str,
|
provider_type: &str,
|
||||||
) -> Result<Option<StoredOAuthProviderConfig>, DataLayerError> {
|
) -> Result<Option<StoredOAuthProviderConfig>, DataLayerError> {
|
||||||
let row = sqlx::query(GET_OAUTH_PROVIDER_CONFIG_SQL)
|
let mut builder = QueryBuilder::<Sqlite>::new(OAUTH_PROVIDER_COLUMNS);
|
||||||
.bind(provider_type)
|
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()
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_sql_err()?;
|
.map_sql_err()?;
|
||||||
@@ -32,7 +42,7 @@ impl SqliteOAuthProviderRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const LIST_OAUTH_PROVIDER_CONFIGS_SQL: &str = r#"
|
const OAUTH_PROVIDER_COLUMNS: &str = r#"
|
||||||
SELECT
|
SELECT
|
||||||
provider_type,
|
provider_type,
|
||||||
display_name,
|
display_name,
|
||||||
@@ -50,29 +60,6 @@ SELECT
|
|||||||
created_at AS created_at_unix_ms,
|
created_at AS created_at_unix_ms,
|
||||||
updated_at AS updated_at_unix_secs
|
updated_at AS updated_at_unix_secs
|
||||||
FROM oauth_providers
|
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#"
|
const COUNT_LOCKED_USERS_IF_PROVIDER_DISABLED_SQL: &str = r#"
|
||||||
@@ -117,10 +104,9 @@ impl OAuthProviderReadRepository for SqliteOAuthProviderRepository {
|
|||||||
async fn list_oauth_provider_configs(
|
async fn list_oauth_provider_configs(
|
||||||
&self,
|
&self,
|
||||||
) -> Result<Vec<StoredOAuthProviderConfig>, DataLayerError> {
|
) -> Result<Vec<StoredOAuthProviderConfig>, DataLayerError> {
|
||||||
let rows = sqlx::query(LIST_OAUTH_PROVIDER_CONFIGS_SQL)
|
let mut builder = QueryBuilder::<Sqlite>::new(OAUTH_PROVIDER_COLUMNS);
|
||||||
.fetch_all(&self.pool)
|
builder.push(" ORDER BY provider_type ASC");
|
||||||
.await
|
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
||||||
.map_sql_err()?;
|
|
||||||
rows.iter().map(map_oauth_provider_row).collect()
|
rows.iter().map(map_oauth_provider_row).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use super::{
|
|||||||
use crate::error::SqlResultExt;
|
use crate::error::SqlResultExt;
|
||||||
use crate::repository::pool_scores::merge_score_reason_patch;
|
use crate::repository::pool_scores::merge_score_reason_patch;
|
||||||
use crate::DataLayerError;
|
use crate::DataLayerError;
|
||||||
|
use aether_data_query::{push_eq, push_in, push_limit, push_limit_offset, WhereClause};
|
||||||
|
|
||||||
const SCORE_COLUMNS: &str = r#"
|
const SCORE_COLUMNS: &str = r#"
|
||||||
SELECT
|
SELECT
|
||||||
@@ -57,25 +58,54 @@ impl MysqlPoolMemberScoreRepository {
|
|||||||
scope: Option<&PoolScoreScope>,
|
scope: Option<&PoolScoreScope>,
|
||||||
) -> Result<Vec<StoredPoolMemberScore>, DataLayerError> {
|
) -> Result<Vec<StoredPoolMemberScore>, DataLayerError> {
|
||||||
let mut builder = QueryBuilder::<MySql>::new(SCORE_COLUMNS);
|
let mut builder = QueryBuilder::<MySql>::new(SCORE_COLUMNS);
|
||||||
builder
|
let mut where_clause = WhereClause::new();
|
||||||
.push(" WHERE pool_kind = ")
|
push_eq(
|
||||||
.push_bind(identity.pool_kind.clone())
|
&mut builder,
|
||||||
.push(" AND pool_id = ")
|
&mut where_clause,
|
||||||
.push_bind(identity.pool_id.clone())
|
"pool_kind",
|
||||||
.push(" AND member_kind = ")
|
identity.pool_kind.clone(),
|
||||||
.push_bind(identity.member_kind.clone())
|
);
|
||||||
.push(" AND member_id = ")
|
push_eq(
|
||||||
.push_bind(identity.member_id.clone());
|
&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(),
|
||||||
|
);
|
||||||
if let Some(scope) = scope {
|
if let Some(scope) = scope {
|
||||||
builder
|
push_eq(
|
||||||
.push(" AND capability = ")
|
&mut builder,
|
||||||
.push_bind(scope.capability.clone())
|
&mut where_clause,
|
||||||
.push(" AND scope_kind = ")
|
"capability",
|
||||||
.push_bind(scope.scope_kind.clone());
|
scope.capability.clone(),
|
||||||
|
);
|
||||||
|
push_eq(
|
||||||
|
&mut builder,
|
||||||
|
&mut where_clause,
|
||||||
|
"scope_kind",
|
||||||
|
scope.scope_kind.clone(),
|
||||||
|
);
|
||||||
if let Some(scope_id) = &scope.scope_id {
|
if let Some(scope_id) = &scope.scope_id {
|
||||||
builder.push(" AND scope_id = ").push_bind(scope_id.clone());
|
push_eq(
|
||||||
|
&mut builder,
|
||||||
|
&mut where_clause,
|
||||||
|
"scope_id",
|
||||||
|
scope_id.clone(),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
builder.push(" AND scope_id IS NULL");
|
where_clause.push_next(&mut builder);
|
||||||
|
builder.push("scope_id IS NULL");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
||||||
@@ -90,44 +120,65 @@ impl PoolScoreReadRepository for MysqlPoolMemberScoreRepository {
|
|||||||
query: &ListRankedPoolMembersQuery,
|
query: &ListRankedPoolMembersQuery,
|
||||||
) -> Result<Vec<StoredPoolMemberScore>, DataLayerError> {
|
) -> Result<Vec<StoredPoolMemberScore>, DataLayerError> {
|
||||||
let mut builder = QueryBuilder::<MySql>::new(SCORE_COLUMNS);
|
let mut builder = QueryBuilder::<MySql>::new(SCORE_COLUMNS);
|
||||||
builder
|
let mut where_clause = WhereClause::new();
|
||||||
.push(" WHERE pool_kind = ")
|
push_eq(
|
||||||
.push_bind(query.pool_kind.clone())
|
&mut builder,
|
||||||
.push(" AND pool_id = ")
|
&mut where_clause,
|
||||||
.push_bind(query.pool_id.clone())
|
"pool_kind",
|
||||||
.push(" AND capability = ")
|
query.pool_kind.clone(),
|
||||||
.push_bind(query.capability.clone())
|
);
|
||||||
.push(" AND scope_kind = ")
|
push_eq(
|
||||||
.push_bind(query.scope_kind.clone());
|
&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(),
|
||||||
|
);
|
||||||
if let Some(scope_id) = &query.scope_id {
|
if let Some(scope_id) = &query.scope_id {
|
||||||
builder.push(" AND scope_id = ").push_bind(scope_id.clone());
|
push_eq(
|
||||||
|
&mut builder,
|
||||||
|
&mut where_clause,
|
||||||
|
"scope_id",
|
||||||
|
scope_id.clone(),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
builder.push(" AND scope_id IS NULL");
|
where_clause.push_next(&mut builder);
|
||||||
|
builder.push("scope_id IS NULL");
|
||||||
}
|
}
|
||||||
if !query.hard_states.is_empty() {
|
if !query.hard_states.is_empty() {
|
||||||
builder.push(" AND hard_state IN (");
|
let states = query
|
||||||
let mut separated = builder.separated(", ");
|
.hard_states
|
||||||
for state in &query.hard_states {
|
.iter()
|
||||||
separated.push_bind(state.as_database());
|
.map(|state| state.as_database())
|
||||||
}
|
.collect::<Vec<_>>();
|
||||||
separated.push_unseparated(")");
|
push_in(&mut builder, &mut where_clause, "hard_state", &states);
|
||||||
}
|
}
|
||||||
if let Some(statuses) = &query.probe_statuses {
|
if let Some(statuses) = &query.probe_statuses {
|
||||||
if !statuses.is_empty() {
|
if !statuses.is_empty() {
|
||||||
builder.push(" AND probe_status IN (");
|
let statuses = statuses
|
||||||
let mut separated = builder.separated(", ");
|
.iter()
|
||||||
for status in statuses {
|
.map(|status| status.as_database())
|
||||||
separated.push_bind(status.as_database());
|
.collect::<Vec<_>>();
|
||||||
}
|
push_in(&mut builder, &mut where_clause, "probe_status", &statuses);
|
||||||
separated.push_unseparated(")");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
builder
|
builder.push(" ORDER BY score DESC, last_ranked_at DESC, member_id ASC, id ASC");
|
||||||
.push(" ORDER BY score DESC, last_ranked_at DESC, member_id ASC, id ASC")
|
push_limit_offset(
|
||||||
.push(" LIMIT ")
|
&mut builder,
|
||||||
.push_bind(i64_from_usize(query.limit.max(1), "pool score limit")?)
|
i64_from_usize(query.limit.max(1), "pool score limit")?,
|
||||||
.push(" OFFSET ")
|
i64_from_usize(query.offset, "pool score offset")?,
|
||||||
.push_bind(i64_from_usize(query.offset, "pool score offset")?);
|
);
|
||||||
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
||||||
rows.iter().map(map_score_row).collect()
|
rows.iter().map(map_score_row).collect()
|
||||||
}
|
}
|
||||||
@@ -137,48 +188,66 @@ impl PoolScoreReadRepository for MysqlPoolMemberScoreRepository {
|
|||||||
query: &ListPoolMemberScoresQuery,
|
query: &ListPoolMemberScoresQuery,
|
||||||
) -> Result<Vec<StoredPoolMemberScore>, DataLayerError> {
|
) -> Result<Vec<StoredPoolMemberScore>, DataLayerError> {
|
||||||
let mut builder = QueryBuilder::<MySql>::new(SCORE_COLUMNS);
|
let mut builder = QueryBuilder::<MySql>::new(SCORE_COLUMNS);
|
||||||
builder
|
let mut where_clause = WhereClause::new();
|
||||||
.push(" WHERE pool_kind = ")
|
push_eq(
|
||||||
.push_bind(query.pool_kind.clone())
|
&mut builder,
|
||||||
.push(" AND pool_id = ")
|
&mut where_clause,
|
||||||
.push_bind(query.pool_id.clone());
|
"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 {
|
if let Some(capability) = &query.capability {
|
||||||
builder
|
push_eq(
|
||||||
.push(" AND capability = ")
|
&mut builder,
|
||||||
.push_bind(capability.clone());
|
&mut where_clause,
|
||||||
|
"capability",
|
||||||
|
capability.clone(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if let Some(scope_kind) = &query.scope_kind {
|
if let Some(scope_kind) = &query.scope_kind {
|
||||||
builder
|
push_eq(
|
||||||
.push(" AND scope_kind = ")
|
&mut builder,
|
||||||
.push_bind(scope_kind.clone());
|
&mut where_clause,
|
||||||
|
"scope_kind",
|
||||||
|
scope_kind.clone(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if let Some(scope_id) = &query.scope_id {
|
if let Some(scope_id) = &query.scope_id {
|
||||||
builder.push(" AND scope_id = ").push_bind(scope_id.clone());
|
push_eq(
|
||||||
|
&mut builder,
|
||||||
|
&mut where_clause,
|
||||||
|
"scope_id",
|
||||||
|
scope_id.clone(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if !query.hard_states.is_empty() {
|
if !query.hard_states.is_empty() {
|
||||||
builder.push(" AND hard_state IN (");
|
let states = query
|
||||||
let mut separated = builder.separated(", ");
|
.hard_states
|
||||||
for state in &query.hard_states {
|
.iter()
|
||||||
separated.push_bind(state.as_database());
|
.map(|state| state.as_database())
|
||||||
}
|
.collect::<Vec<_>>();
|
||||||
separated.push_unseparated(")");
|
push_in(&mut builder, &mut where_clause, "hard_state", &states);
|
||||||
}
|
}
|
||||||
if let Some(statuses) = &query.probe_statuses {
|
if let Some(statuses) = &query.probe_statuses {
|
||||||
if !statuses.is_empty() {
|
if !statuses.is_empty() {
|
||||||
builder.push(" AND probe_status IN (");
|
let statuses = statuses
|
||||||
let mut separated = builder.separated(", ");
|
.iter()
|
||||||
for status in statuses {
|
.map(|status| status.as_database())
|
||||||
separated.push_bind(status.as_database());
|
.collect::<Vec<_>>();
|
||||||
}
|
push_in(&mut builder, &mut where_clause, "probe_status", &statuses);
|
||||||
separated.push_unseparated(")");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
builder
|
builder.push(" ORDER BY score DESC, last_ranked_at DESC, member_id ASC, id ASC");
|
||||||
.push(" ORDER BY score DESC, last_ranked_at DESC, member_id ASC, id ASC")
|
push_limit_offset(
|
||||||
.push(" LIMIT ")
|
&mut builder,
|
||||||
.push_bind(i64_from_usize(query.limit.max(1), "pool score limit")?)
|
i64_from_usize(query.limit.max(1), "pool score limit")?,
|
||||||
.push(" OFFSET ")
|
i64_from_usize(query.offset, "pool score offset")?,
|
||||||
.push_bind(i64_from_usize(query.offset, "pool score offset")?);
|
);
|
||||||
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
||||||
rows.iter().map(map_score_row).collect()
|
rows.iter().map(map_score_row).collect()
|
||||||
}
|
}
|
||||||
@@ -188,18 +257,30 @@ impl PoolScoreReadRepository for MysqlPoolMemberScoreRepository {
|
|||||||
query: &ListPoolMemberProbeCandidatesQuery,
|
query: &ListPoolMemberProbeCandidatesQuery,
|
||||||
) -> Result<Vec<StoredPoolMemberScore>, DataLayerError> {
|
) -> Result<Vec<StoredPoolMemberScore>, DataLayerError> {
|
||||||
let mut builder = QueryBuilder::<MySql>::new(SCORE_COLUMNS);
|
let mut builder = QueryBuilder::<MySql>::new(SCORE_COLUMNS);
|
||||||
builder
|
let mut where_clause = WhereClause::new();
|
||||||
.push(" WHERE pool_kind = ")
|
push_eq(
|
||||||
.push_bind(query.pool_kind.clone())
|
&mut builder,
|
||||||
.push(" AND pool_id = ")
|
&mut where_clause,
|
||||||
.push_bind(query.pool_id.clone());
|
"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 {
|
if let Some(capability) = &query.capability {
|
||||||
builder
|
push_eq(
|
||||||
.push(" AND capability = ")
|
&mut builder,
|
||||||
.push_bind(capability.clone());
|
&mut where_clause,
|
||||||
|
"capability",
|
||||||
|
capability.clone(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
where_clause.push_next(&mut builder);
|
||||||
builder
|
builder
|
||||||
.push(" AND hard_state IN ('available','unknown','cooldown','quota_exhausted')")
|
.push("hard_state IN ('available','unknown','cooldown','quota_exhausted')")
|
||||||
.push(" AND (probe_status IN ('never','failed','stale')")
|
.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(" OR (probe_status = 'ok' AND (last_probe_success_at IS NULL OR last_probe_success_at <= ")
|
||||||
.push_bind(i64_from_u64(
|
.push_bind(i64_from_u64(
|
||||||
@@ -228,12 +309,11 @@ impl PoolScoreReadRepository for MysqlPoolMemberScoreRepository {
|
|||||||
COALESCE(last_scheduled_at, 0) DESC,
|
COALESCE(last_scheduled_at, 0) DESC,
|
||||||
member_id ASC
|
member_id ASC
|
||||||
"#,
|
"#,
|
||||||
)
|
);
|
||||||
.push(" LIMIT ")
|
push_limit(
|
||||||
.push_bind(i64_from_usize(
|
&mut builder,
|
||||||
query.limit.max(1),
|
i64_from_usize(query.limit.max(1), "pool probe candidate limit")?,
|
||||||
"pool probe candidate limit",
|
);
|
||||||
)?);
|
|
||||||
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
||||||
rows.iter().map(map_score_row).collect()
|
rows.iter().map(map_score_row).collect()
|
||||||
}
|
}
|
||||||
@@ -246,12 +326,8 @@ impl PoolScoreReadRepository for MysqlPoolMemberScoreRepository {
|
|||||||
return Ok(Vec::new());
|
return Ok(Vec::new());
|
||||||
}
|
}
|
||||||
let mut builder = QueryBuilder::<MySql>::new(SCORE_COLUMNS);
|
let mut builder = QueryBuilder::<MySql>::new(SCORE_COLUMNS);
|
||||||
builder.push(" WHERE id IN (");
|
let mut where_clause = WhereClause::new();
|
||||||
let mut separated = builder.separated(", ");
|
push_in(&mut builder, &mut where_clause, "id", &query.ids);
|
||||||
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()?;
|
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
||||||
rows.iter().map(map_score_row).collect()
|
rows.iter().map(map_score_row).collect()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use super::{
|
|||||||
use crate::error::SqlxResultExt;
|
use crate::error::SqlxResultExt;
|
||||||
use crate::repository::pool_scores::merge_score_reason_patch;
|
use crate::repository::pool_scores::merge_score_reason_patch;
|
||||||
use crate::DataLayerError;
|
use crate::DataLayerError;
|
||||||
|
use aether_data_query::{push_eq, push_in, push_limit, push_limit_offset, WhereClause};
|
||||||
|
|
||||||
const SCORE_COLUMNS: &str = r#"
|
const SCORE_COLUMNS: &str = r#"
|
||||||
SELECT
|
SELECT
|
||||||
@@ -57,25 +58,54 @@ impl PostgresPoolMemberScoreRepository {
|
|||||||
scope: Option<&PoolScoreScope>,
|
scope: Option<&PoolScoreScope>,
|
||||||
) -> Result<Vec<StoredPoolMemberScore>, DataLayerError> {
|
) -> Result<Vec<StoredPoolMemberScore>, DataLayerError> {
|
||||||
let mut builder = QueryBuilder::<Postgres>::new(SCORE_COLUMNS);
|
let mut builder = QueryBuilder::<Postgres>::new(SCORE_COLUMNS);
|
||||||
builder
|
let mut where_clause = WhereClause::new();
|
||||||
.push(" WHERE pool_kind = ")
|
push_eq(
|
||||||
.push_bind(identity.pool_kind.clone())
|
&mut builder,
|
||||||
.push(" AND pool_id = ")
|
&mut where_clause,
|
||||||
.push_bind(identity.pool_id.clone())
|
"pool_kind",
|
||||||
.push(" AND member_kind = ")
|
identity.pool_kind.clone(),
|
||||||
.push_bind(identity.member_kind.clone())
|
);
|
||||||
.push(" AND member_id = ")
|
push_eq(
|
||||||
.push_bind(identity.member_id.clone());
|
&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(),
|
||||||
|
);
|
||||||
if let Some(scope) = scope {
|
if let Some(scope) = scope {
|
||||||
builder
|
push_eq(
|
||||||
.push(" AND capability = ")
|
&mut builder,
|
||||||
.push_bind(scope.capability.clone())
|
&mut where_clause,
|
||||||
.push(" AND scope_kind = ")
|
"capability",
|
||||||
.push_bind(scope.scope_kind.clone());
|
scope.capability.clone(),
|
||||||
|
);
|
||||||
|
push_eq(
|
||||||
|
&mut builder,
|
||||||
|
&mut where_clause,
|
||||||
|
"scope_kind",
|
||||||
|
scope.scope_kind.clone(),
|
||||||
|
);
|
||||||
if let Some(scope_id) = &scope.scope_id {
|
if let Some(scope_id) = &scope.scope_id {
|
||||||
builder.push(" AND scope_id = ").push_bind(scope_id.clone());
|
push_eq(
|
||||||
|
&mut builder,
|
||||||
|
&mut where_clause,
|
||||||
|
"scope_id",
|
||||||
|
scope_id.clone(),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
builder.push(" AND scope_id IS NULL");
|
where_clause.push_next(&mut builder);
|
||||||
|
builder.push("scope_id IS NULL");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let rows = builder
|
let rows = builder
|
||||||
@@ -94,44 +124,65 @@ impl PoolScoreReadRepository for PostgresPoolMemberScoreRepository {
|
|||||||
query: &ListRankedPoolMembersQuery,
|
query: &ListRankedPoolMembersQuery,
|
||||||
) -> Result<Vec<StoredPoolMemberScore>, DataLayerError> {
|
) -> Result<Vec<StoredPoolMemberScore>, DataLayerError> {
|
||||||
let mut builder = QueryBuilder::<Postgres>::new(SCORE_COLUMNS);
|
let mut builder = QueryBuilder::<Postgres>::new(SCORE_COLUMNS);
|
||||||
builder
|
let mut where_clause = WhereClause::new();
|
||||||
.push(" WHERE pool_kind = ")
|
push_eq(
|
||||||
.push_bind(query.pool_kind.clone())
|
&mut builder,
|
||||||
.push(" AND pool_id = ")
|
&mut where_clause,
|
||||||
.push_bind(query.pool_id.clone())
|
"pool_kind",
|
||||||
.push(" AND capability = ")
|
query.pool_kind.clone(),
|
||||||
.push_bind(query.capability.clone())
|
);
|
||||||
.push(" AND scope_kind = ")
|
push_eq(
|
||||||
.push_bind(query.scope_kind.clone());
|
&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(),
|
||||||
|
);
|
||||||
if let Some(scope_id) = &query.scope_id {
|
if let Some(scope_id) = &query.scope_id {
|
||||||
builder.push(" AND scope_id = ").push_bind(scope_id.clone());
|
push_eq(
|
||||||
|
&mut builder,
|
||||||
|
&mut where_clause,
|
||||||
|
"scope_id",
|
||||||
|
scope_id.clone(),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
builder.push(" AND scope_id IS NULL");
|
where_clause.push_next(&mut builder);
|
||||||
|
builder.push("scope_id IS NULL");
|
||||||
}
|
}
|
||||||
if !query.hard_states.is_empty() {
|
if !query.hard_states.is_empty() {
|
||||||
builder.push(" AND hard_state IN (");
|
let states = query
|
||||||
let mut separated = builder.separated(", ");
|
.hard_states
|
||||||
for state in &query.hard_states {
|
.iter()
|
||||||
separated.push_bind(state.as_database());
|
.map(|state| state.as_database())
|
||||||
}
|
.collect::<Vec<_>>();
|
||||||
separated.push_unseparated(")");
|
push_in(&mut builder, &mut where_clause, "hard_state", &states);
|
||||||
}
|
}
|
||||||
if let Some(statuses) = &query.probe_statuses {
|
if let Some(statuses) = &query.probe_statuses {
|
||||||
if !statuses.is_empty() {
|
if !statuses.is_empty() {
|
||||||
builder.push(" AND probe_status IN (");
|
let statuses = statuses
|
||||||
let mut separated = builder.separated(", ");
|
.iter()
|
||||||
for status in statuses {
|
.map(|status| status.as_database())
|
||||||
separated.push_bind(status.as_database());
|
.collect::<Vec<_>>();
|
||||||
}
|
push_in(&mut builder, &mut where_clause, "probe_status", &statuses);
|
||||||
separated.push_unseparated(")");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
builder
|
builder.push(" ORDER BY score DESC, last_ranked_at DESC NULLS LAST, member_id ASC, id ASC");
|
||||||
.push(" ORDER BY score DESC, last_ranked_at DESC NULLS LAST, member_id ASC, id ASC")
|
push_limit_offset(
|
||||||
.push(" LIMIT ")
|
&mut builder,
|
||||||
.push_bind(i64_from_usize(query.limit.max(1), "pool score limit")?)
|
i64_from_usize(query.limit.max(1), "pool score limit")?,
|
||||||
.push(" OFFSET ")
|
i64_from_usize(query.offset, "pool score offset")?,
|
||||||
.push_bind(i64_from_usize(query.offset, "pool score offset")?);
|
);
|
||||||
let rows = builder
|
let rows = builder
|
||||||
.build()
|
.build()
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
@@ -145,48 +196,66 @@ impl PoolScoreReadRepository for PostgresPoolMemberScoreRepository {
|
|||||||
query: &ListPoolMemberScoresQuery,
|
query: &ListPoolMemberScoresQuery,
|
||||||
) -> Result<Vec<StoredPoolMemberScore>, DataLayerError> {
|
) -> Result<Vec<StoredPoolMemberScore>, DataLayerError> {
|
||||||
let mut builder = QueryBuilder::<Postgres>::new(SCORE_COLUMNS);
|
let mut builder = QueryBuilder::<Postgres>::new(SCORE_COLUMNS);
|
||||||
builder
|
let mut where_clause = WhereClause::new();
|
||||||
.push(" WHERE pool_kind = ")
|
push_eq(
|
||||||
.push_bind(query.pool_kind.clone())
|
&mut builder,
|
||||||
.push(" AND pool_id = ")
|
&mut where_clause,
|
||||||
.push_bind(query.pool_id.clone());
|
"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 {
|
if let Some(capability) = &query.capability {
|
||||||
builder
|
push_eq(
|
||||||
.push(" AND capability = ")
|
&mut builder,
|
||||||
.push_bind(capability.clone());
|
&mut where_clause,
|
||||||
|
"capability",
|
||||||
|
capability.clone(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if let Some(scope_kind) = &query.scope_kind {
|
if let Some(scope_kind) = &query.scope_kind {
|
||||||
builder
|
push_eq(
|
||||||
.push(" AND scope_kind = ")
|
&mut builder,
|
||||||
.push_bind(scope_kind.clone());
|
&mut where_clause,
|
||||||
|
"scope_kind",
|
||||||
|
scope_kind.clone(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if let Some(scope_id) = &query.scope_id {
|
if let Some(scope_id) = &query.scope_id {
|
||||||
builder.push(" AND scope_id = ").push_bind(scope_id.clone());
|
push_eq(
|
||||||
|
&mut builder,
|
||||||
|
&mut where_clause,
|
||||||
|
"scope_id",
|
||||||
|
scope_id.clone(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if !query.hard_states.is_empty() {
|
if !query.hard_states.is_empty() {
|
||||||
builder.push(" AND hard_state IN (");
|
let states = query
|
||||||
let mut separated = builder.separated(", ");
|
.hard_states
|
||||||
for state in &query.hard_states {
|
.iter()
|
||||||
separated.push_bind(state.as_database());
|
.map(|state| state.as_database())
|
||||||
}
|
.collect::<Vec<_>>();
|
||||||
separated.push_unseparated(")");
|
push_in(&mut builder, &mut where_clause, "hard_state", &states);
|
||||||
}
|
}
|
||||||
if let Some(statuses) = &query.probe_statuses {
|
if let Some(statuses) = &query.probe_statuses {
|
||||||
if !statuses.is_empty() {
|
if !statuses.is_empty() {
|
||||||
builder.push(" AND probe_status IN (");
|
let statuses = statuses
|
||||||
let mut separated = builder.separated(", ");
|
.iter()
|
||||||
for status in statuses {
|
.map(|status| status.as_database())
|
||||||
separated.push_bind(status.as_database());
|
.collect::<Vec<_>>();
|
||||||
}
|
push_in(&mut builder, &mut where_clause, "probe_status", &statuses);
|
||||||
separated.push_unseparated(")");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
builder
|
builder.push(" ORDER BY score DESC, last_ranked_at DESC NULLS LAST, member_id ASC, id ASC");
|
||||||
.push(" ORDER BY score DESC, last_ranked_at DESC NULLS LAST, member_id ASC, id ASC")
|
push_limit_offset(
|
||||||
.push(" LIMIT ")
|
&mut builder,
|
||||||
.push_bind(i64_from_usize(query.limit.max(1), "pool score limit")?)
|
i64_from_usize(query.limit.max(1), "pool score limit")?,
|
||||||
.push(" OFFSET ")
|
i64_from_usize(query.offset, "pool score offset")?,
|
||||||
.push_bind(i64_from_usize(query.offset, "pool score offset")?);
|
);
|
||||||
let rows = builder
|
let rows = builder
|
||||||
.build()
|
.build()
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
@@ -200,18 +269,30 @@ impl PoolScoreReadRepository for PostgresPoolMemberScoreRepository {
|
|||||||
query: &ListPoolMemberProbeCandidatesQuery,
|
query: &ListPoolMemberProbeCandidatesQuery,
|
||||||
) -> Result<Vec<StoredPoolMemberScore>, DataLayerError> {
|
) -> Result<Vec<StoredPoolMemberScore>, DataLayerError> {
|
||||||
let mut builder = QueryBuilder::<Postgres>::new(SCORE_COLUMNS);
|
let mut builder = QueryBuilder::<Postgres>::new(SCORE_COLUMNS);
|
||||||
builder
|
let mut where_clause = WhereClause::new();
|
||||||
.push(" WHERE pool_kind = ")
|
push_eq(
|
||||||
.push_bind(query.pool_kind.clone())
|
&mut builder,
|
||||||
.push(" AND pool_id = ")
|
&mut where_clause,
|
||||||
.push_bind(query.pool_id.clone());
|
"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 {
|
if let Some(capability) = &query.capability {
|
||||||
builder
|
push_eq(
|
||||||
.push(" AND capability = ")
|
&mut builder,
|
||||||
.push_bind(capability.clone());
|
&mut where_clause,
|
||||||
|
"capability",
|
||||||
|
capability.clone(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
where_clause.push_next(&mut builder);
|
||||||
builder
|
builder
|
||||||
.push(" AND hard_state IN ('available','unknown','cooldown','quota_exhausted')")
|
.push("hard_state IN ('available','unknown','cooldown','quota_exhausted')")
|
||||||
.push(" AND (probe_status IN ('never','failed','stale')")
|
.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(" OR (probe_status = 'ok' AND (last_probe_success_at IS NULL OR last_probe_success_at <= ")
|
||||||
.push_bind(i64_from_u64(
|
.push_bind(i64_from_u64(
|
||||||
@@ -240,12 +321,11 @@ impl PoolScoreReadRepository for PostgresPoolMemberScoreRepository {
|
|||||||
COALESCE(last_scheduled_at, 0) DESC,
|
COALESCE(last_scheduled_at, 0) DESC,
|
||||||
member_id ASC
|
member_id ASC
|
||||||
"#,
|
"#,
|
||||||
)
|
);
|
||||||
.push(" LIMIT ")
|
push_limit(
|
||||||
.push_bind(i64_from_usize(
|
&mut builder,
|
||||||
query.limit.max(1),
|
i64_from_usize(query.limit.max(1), "pool probe candidate limit")?,
|
||||||
"pool probe candidate limit",
|
);
|
||||||
)?);
|
|
||||||
let rows = builder
|
let rows = builder
|
||||||
.build()
|
.build()
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
@@ -262,12 +342,8 @@ impl PoolScoreReadRepository for PostgresPoolMemberScoreRepository {
|
|||||||
return Ok(Vec::new());
|
return Ok(Vec::new());
|
||||||
}
|
}
|
||||||
let mut builder = QueryBuilder::<Postgres>::new(SCORE_COLUMNS);
|
let mut builder = QueryBuilder::<Postgres>::new(SCORE_COLUMNS);
|
||||||
builder.push(" WHERE id IN (");
|
let mut where_clause = WhereClause::new();
|
||||||
let mut separated = builder.separated(", ");
|
push_in(&mut builder, &mut where_clause, "id", &query.ids);
|
||||||
for id in &query.ids {
|
|
||||||
separated.push_bind(id.clone());
|
|
||||||
}
|
|
||||||
separated.push_unseparated(")");
|
|
||||||
let rows = builder
|
let rows = builder
|
||||||
.build()
|
.build()
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use super::{
|
|||||||
use crate::error::SqlResultExt;
|
use crate::error::SqlResultExt;
|
||||||
use crate::repository::pool_scores::merge_score_reason_patch;
|
use crate::repository::pool_scores::merge_score_reason_patch;
|
||||||
use crate::DataLayerError;
|
use crate::DataLayerError;
|
||||||
|
use aether_data_query::{push_eq, push_in, push_limit, push_limit_offset, WhereClause};
|
||||||
|
|
||||||
const SCORE_COLUMNS: &str = r#"
|
const SCORE_COLUMNS: &str = r#"
|
||||||
SELECT
|
SELECT
|
||||||
@@ -57,25 +58,54 @@ impl SqlitePoolMemberScoreRepository {
|
|||||||
scope: Option<&PoolScoreScope>,
|
scope: Option<&PoolScoreScope>,
|
||||||
) -> Result<Vec<StoredPoolMemberScore>, DataLayerError> {
|
) -> Result<Vec<StoredPoolMemberScore>, DataLayerError> {
|
||||||
let mut builder = QueryBuilder::<Sqlite>::new(SCORE_COLUMNS);
|
let mut builder = QueryBuilder::<Sqlite>::new(SCORE_COLUMNS);
|
||||||
builder
|
let mut where_clause = WhereClause::new();
|
||||||
.push(" WHERE pool_kind = ")
|
push_eq(
|
||||||
.push_bind(identity.pool_kind.clone())
|
&mut builder,
|
||||||
.push(" AND pool_id = ")
|
&mut where_clause,
|
||||||
.push_bind(identity.pool_id.clone())
|
"pool_kind",
|
||||||
.push(" AND member_kind = ")
|
identity.pool_kind.clone(),
|
||||||
.push_bind(identity.member_kind.clone())
|
);
|
||||||
.push(" AND member_id = ")
|
push_eq(
|
||||||
.push_bind(identity.member_id.clone());
|
&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(),
|
||||||
|
);
|
||||||
if let Some(scope) = scope {
|
if let Some(scope) = scope {
|
||||||
builder
|
push_eq(
|
||||||
.push(" AND capability = ")
|
&mut builder,
|
||||||
.push_bind(scope.capability.clone())
|
&mut where_clause,
|
||||||
.push(" AND scope_kind = ")
|
"capability",
|
||||||
.push_bind(scope.scope_kind.clone());
|
scope.capability.clone(),
|
||||||
|
);
|
||||||
|
push_eq(
|
||||||
|
&mut builder,
|
||||||
|
&mut where_clause,
|
||||||
|
"scope_kind",
|
||||||
|
scope.scope_kind.clone(),
|
||||||
|
);
|
||||||
if let Some(scope_id) = &scope.scope_id {
|
if let Some(scope_id) = &scope.scope_id {
|
||||||
builder.push(" AND scope_id = ").push_bind(scope_id.clone());
|
push_eq(
|
||||||
|
&mut builder,
|
||||||
|
&mut where_clause,
|
||||||
|
"scope_id",
|
||||||
|
scope_id.clone(),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
builder.push(" AND scope_id IS NULL");
|
where_clause.push_next(&mut builder);
|
||||||
|
builder.push("scope_id IS NULL");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
||||||
@@ -90,44 +120,65 @@ impl PoolScoreReadRepository for SqlitePoolMemberScoreRepository {
|
|||||||
query: &ListRankedPoolMembersQuery,
|
query: &ListRankedPoolMembersQuery,
|
||||||
) -> Result<Vec<StoredPoolMemberScore>, DataLayerError> {
|
) -> Result<Vec<StoredPoolMemberScore>, DataLayerError> {
|
||||||
let mut builder = QueryBuilder::<Sqlite>::new(SCORE_COLUMNS);
|
let mut builder = QueryBuilder::<Sqlite>::new(SCORE_COLUMNS);
|
||||||
builder
|
let mut where_clause = WhereClause::new();
|
||||||
.push(" WHERE pool_kind = ")
|
push_eq(
|
||||||
.push_bind(query.pool_kind.clone())
|
&mut builder,
|
||||||
.push(" AND pool_id = ")
|
&mut where_clause,
|
||||||
.push_bind(query.pool_id.clone())
|
"pool_kind",
|
||||||
.push(" AND capability = ")
|
query.pool_kind.clone(),
|
||||||
.push_bind(query.capability.clone())
|
);
|
||||||
.push(" AND scope_kind = ")
|
push_eq(
|
||||||
.push_bind(query.scope_kind.clone());
|
&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(),
|
||||||
|
);
|
||||||
if let Some(scope_id) = &query.scope_id {
|
if let Some(scope_id) = &query.scope_id {
|
||||||
builder.push(" AND scope_id = ").push_bind(scope_id.clone());
|
push_eq(
|
||||||
|
&mut builder,
|
||||||
|
&mut where_clause,
|
||||||
|
"scope_id",
|
||||||
|
scope_id.clone(),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
builder.push(" AND scope_id IS NULL");
|
where_clause.push_next(&mut builder);
|
||||||
|
builder.push("scope_id IS NULL");
|
||||||
}
|
}
|
||||||
if !query.hard_states.is_empty() {
|
if !query.hard_states.is_empty() {
|
||||||
builder.push(" AND hard_state IN (");
|
let states = query
|
||||||
let mut separated = builder.separated(", ");
|
.hard_states
|
||||||
for state in &query.hard_states {
|
.iter()
|
||||||
separated.push_bind(state.as_database());
|
.map(|state| state.as_database())
|
||||||
}
|
.collect::<Vec<_>>();
|
||||||
separated.push_unseparated(")");
|
push_in(&mut builder, &mut where_clause, "hard_state", &states);
|
||||||
}
|
}
|
||||||
if let Some(statuses) = &query.probe_statuses {
|
if let Some(statuses) = &query.probe_statuses {
|
||||||
if !statuses.is_empty() {
|
if !statuses.is_empty() {
|
||||||
builder.push(" AND probe_status IN (");
|
let statuses = statuses
|
||||||
let mut separated = builder.separated(", ");
|
.iter()
|
||||||
for status in statuses {
|
.map(|status| status.as_database())
|
||||||
separated.push_bind(status.as_database());
|
.collect::<Vec<_>>();
|
||||||
}
|
push_in(&mut builder, &mut where_clause, "probe_status", &statuses);
|
||||||
separated.push_unseparated(")");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
builder
|
builder.push(" ORDER BY score DESC, last_ranked_at DESC, member_id ASC, id ASC");
|
||||||
.push(" ORDER BY score DESC, last_ranked_at DESC, member_id ASC, id ASC")
|
push_limit_offset(
|
||||||
.push(" LIMIT ")
|
&mut builder,
|
||||||
.push_bind(i64_from_usize(query.limit.max(1), "pool score limit")?)
|
i64_from_usize(query.limit.max(1), "pool score limit")?,
|
||||||
.push(" OFFSET ")
|
i64_from_usize(query.offset, "pool score offset")?,
|
||||||
.push_bind(i64_from_usize(query.offset, "pool score offset")?);
|
);
|
||||||
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
||||||
rows.iter().map(map_score_row).collect()
|
rows.iter().map(map_score_row).collect()
|
||||||
}
|
}
|
||||||
@@ -137,48 +188,66 @@ impl PoolScoreReadRepository for SqlitePoolMemberScoreRepository {
|
|||||||
query: &ListPoolMemberScoresQuery,
|
query: &ListPoolMemberScoresQuery,
|
||||||
) -> Result<Vec<StoredPoolMemberScore>, DataLayerError> {
|
) -> Result<Vec<StoredPoolMemberScore>, DataLayerError> {
|
||||||
let mut builder = QueryBuilder::<Sqlite>::new(SCORE_COLUMNS);
|
let mut builder = QueryBuilder::<Sqlite>::new(SCORE_COLUMNS);
|
||||||
builder
|
let mut where_clause = WhereClause::new();
|
||||||
.push(" WHERE pool_kind = ")
|
push_eq(
|
||||||
.push_bind(query.pool_kind.clone())
|
&mut builder,
|
||||||
.push(" AND pool_id = ")
|
&mut where_clause,
|
||||||
.push_bind(query.pool_id.clone());
|
"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 {
|
if let Some(capability) = &query.capability {
|
||||||
builder
|
push_eq(
|
||||||
.push(" AND capability = ")
|
&mut builder,
|
||||||
.push_bind(capability.clone());
|
&mut where_clause,
|
||||||
|
"capability",
|
||||||
|
capability.clone(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if let Some(scope_kind) = &query.scope_kind {
|
if let Some(scope_kind) = &query.scope_kind {
|
||||||
builder
|
push_eq(
|
||||||
.push(" AND scope_kind = ")
|
&mut builder,
|
||||||
.push_bind(scope_kind.clone());
|
&mut where_clause,
|
||||||
|
"scope_kind",
|
||||||
|
scope_kind.clone(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if let Some(scope_id) = &query.scope_id {
|
if let Some(scope_id) = &query.scope_id {
|
||||||
builder.push(" AND scope_id = ").push_bind(scope_id.clone());
|
push_eq(
|
||||||
|
&mut builder,
|
||||||
|
&mut where_clause,
|
||||||
|
"scope_id",
|
||||||
|
scope_id.clone(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if !query.hard_states.is_empty() {
|
if !query.hard_states.is_empty() {
|
||||||
builder.push(" AND hard_state IN (");
|
let states = query
|
||||||
let mut separated = builder.separated(", ");
|
.hard_states
|
||||||
for state in &query.hard_states {
|
.iter()
|
||||||
separated.push_bind(state.as_database());
|
.map(|state| state.as_database())
|
||||||
}
|
.collect::<Vec<_>>();
|
||||||
separated.push_unseparated(")");
|
push_in(&mut builder, &mut where_clause, "hard_state", &states);
|
||||||
}
|
}
|
||||||
if let Some(statuses) = &query.probe_statuses {
|
if let Some(statuses) = &query.probe_statuses {
|
||||||
if !statuses.is_empty() {
|
if !statuses.is_empty() {
|
||||||
builder.push(" AND probe_status IN (");
|
let statuses = statuses
|
||||||
let mut separated = builder.separated(", ");
|
.iter()
|
||||||
for status in statuses {
|
.map(|status| status.as_database())
|
||||||
separated.push_bind(status.as_database());
|
.collect::<Vec<_>>();
|
||||||
}
|
push_in(&mut builder, &mut where_clause, "probe_status", &statuses);
|
||||||
separated.push_unseparated(")");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
builder
|
builder.push(" ORDER BY score DESC, last_ranked_at DESC, member_id ASC, id ASC");
|
||||||
.push(" ORDER BY score DESC, last_ranked_at DESC, member_id ASC, id ASC")
|
push_limit_offset(
|
||||||
.push(" LIMIT ")
|
&mut builder,
|
||||||
.push_bind(i64_from_usize(query.limit.max(1), "pool score limit")?)
|
i64_from_usize(query.limit.max(1), "pool score limit")?,
|
||||||
.push(" OFFSET ")
|
i64_from_usize(query.offset, "pool score offset")?,
|
||||||
.push_bind(i64_from_usize(query.offset, "pool score offset")?);
|
);
|
||||||
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
||||||
rows.iter().map(map_score_row).collect()
|
rows.iter().map(map_score_row).collect()
|
||||||
}
|
}
|
||||||
@@ -188,18 +257,30 @@ impl PoolScoreReadRepository for SqlitePoolMemberScoreRepository {
|
|||||||
query: &ListPoolMemberProbeCandidatesQuery,
|
query: &ListPoolMemberProbeCandidatesQuery,
|
||||||
) -> Result<Vec<StoredPoolMemberScore>, DataLayerError> {
|
) -> Result<Vec<StoredPoolMemberScore>, DataLayerError> {
|
||||||
let mut builder = QueryBuilder::<Sqlite>::new(SCORE_COLUMNS);
|
let mut builder = QueryBuilder::<Sqlite>::new(SCORE_COLUMNS);
|
||||||
builder
|
let mut where_clause = WhereClause::new();
|
||||||
.push(" WHERE pool_kind = ")
|
push_eq(
|
||||||
.push_bind(query.pool_kind.clone())
|
&mut builder,
|
||||||
.push(" AND pool_id = ")
|
&mut where_clause,
|
||||||
.push_bind(query.pool_id.clone());
|
"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 {
|
if let Some(capability) = &query.capability {
|
||||||
builder
|
push_eq(
|
||||||
.push(" AND capability = ")
|
&mut builder,
|
||||||
.push_bind(capability.clone());
|
&mut where_clause,
|
||||||
|
"capability",
|
||||||
|
capability.clone(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
where_clause.push_next(&mut builder);
|
||||||
builder
|
builder
|
||||||
.push(" AND hard_state IN ('available','unknown','cooldown','quota_exhausted')")
|
.push("hard_state IN ('available','unknown','cooldown','quota_exhausted')")
|
||||||
.push(" AND (probe_status IN ('never','failed','stale')")
|
.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(" OR (probe_status = 'ok' AND (last_probe_success_at IS NULL OR last_probe_success_at <= ")
|
||||||
.push_bind(i64_from_u64(
|
.push_bind(i64_from_u64(
|
||||||
@@ -228,12 +309,11 @@ impl PoolScoreReadRepository for SqlitePoolMemberScoreRepository {
|
|||||||
COALESCE(last_scheduled_at, 0) DESC,
|
COALESCE(last_scheduled_at, 0) DESC,
|
||||||
member_id ASC
|
member_id ASC
|
||||||
"#,
|
"#,
|
||||||
)
|
);
|
||||||
.push(" LIMIT ")
|
push_limit(
|
||||||
.push_bind(i64_from_usize(
|
&mut builder,
|
||||||
query.limit.max(1),
|
i64_from_usize(query.limit.max(1), "pool probe candidate limit")?,
|
||||||
"pool probe candidate limit",
|
);
|
||||||
)?);
|
|
||||||
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
||||||
rows.iter().map(map_score_row).collect()
|
rows.iter().map(map_score_row).collect()
|
||||||
}
|
}
|
||||||
@@ -246,12 +326,8 @@ impl PoolScoreReadRepository for SqlitePoolMemberScoreRepository {
|
|||||||
return Ok(Vec::new());
|
return Ok(Vec::new());
|
||||||
}
|
}
|
||||||
let mut builder = QueryBuilder::<Sqlite>::new(SCORE_COLUMNS);
|
let mut builder = QueryBuilder::<Sqlite>::new(SCORE_COLUMNS);
|
||||||
builder.push(" WHERE id IN (");
|
let mut where_clause = WhereClause::new();
|
||||||
let mut separated = builder.separated(", ");
|
push_in(&mut builder, &mut where_clause, "id", &query.ids);
|
||||||
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()?;
|
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
||||||
rows.iter().map(map_score_row).collect()
|
rows.iter().map(map_score_row).collect()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ use crate::{
|
|||||||
error::{postgres_error, SqlxResultExt},
|
error::{postgres_error, SqlxResultExt},
|
||||||
DataLayerError,
|
DataLayerError,
|
||||||
};
|
};
|
||||||
|
use aether_data_query::{
|
||||||
|
push_ci_contains_any, push_eq, push_in, push_limit_offset, push_optional_eq, SqlDialect,
|
||||||
|
WhereClause,
|
||||||
|
};
|
||||||
|
|
||||||
const LIST_PROVIDERS_BY_IDS_PREFIX: &str = r#"
|
const LIST_PROVIDERS_BY_IDS_PREFIX: &str = r#"
|
||||||
SELECT
|
SELECT
|
||||||
@@ -358,43 +362,14 @@ impl SqlxProviderCatalogReadRepository {
|
|||||||
&self,
|
&self,
|
||||||
active_only: bool,
|
active_only: bool,
|
||||||
) -> Result<Vec<StoredProviderCatalogProvider>, DataLayerError> {
|
) -> Result<Vec<StoredProviderCatalogProvider>, DataLayerError> {
|
||||||
collect_query_rows(
|
let mut builder =
|
||||||
sqlx::query(
|
QueryBuilder::<Postgres>::new(select_prefix_for_in(LIST_PROVIDERS_BY_IDS_PREFIX));
|
||||||
r#"
|
let mut where_clause = WhereClause::new();
|
||||||
SELECT
|
if active_only {
|
||||||
id,
|
push_eq(&mut builder, &mut where_clause, "is_active", true);
|
||||||
name,
|
}
|
||||||
description,
|
builder.push(" ORDER BY provider_priority ASC, name ASC");
|
||||||
website,
|
collect_query_rows(builder.build().fetch(&self.pool), map_provider_row).await
|
||||||
provider_type,
|
|
||||||
CAST(billing_type AS TEXT) AS billing_type,
|
|
||||||
CAST(monthly_quota_usd AS DOUBLE PRECISION) AS monthly_quota_usd,
|
|
||||||
CAST(monthly_used_usd AS DOUBLE PRECISION) AS monthly_used_usd,
|
|
||||||
quota_reset_day,
|
|
||||||
CAST(EXTRACT(EPOCH FROM quota_last_reset_at) AS BIGINT) AS quota_last_reset_at_unix_secs,
|
|
||||||
CAST(EXTRACT(EPOCH FROM quota_expires_at) AS BIGINT) AS quota_expires_at_unix_secs,
|
|
||||||
provider_priority,
|
|
||||||
is_active,
|
|
||||||
keep_priority_on_conversion,
|
|
||||||
enable_format_conversion,
|
|
||||||
concurrent_limit,
|
|
||||||
max_retries,
|
|
||||||
proxy,
|
|
||||||
request_timeout,
|
|
||||||
stream_first_byte_timeout,
|
|
||||||
config,
|
|
||||||
EXTRACT(EPOCH FROM created_at)::bigint AS created_at_unix_ms,
|
|
||||||
EXTRACT(EPOCH FROM updated_at)::bigint AS updated_at_unix_secs
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_endpoints_by_ids(
|
pub async fn list_endpoints_by_ids(
|
||||||
@@ -560,12 +535,6 @@ ORDER BY provider_priority ASC, name ASC
|
|||||||
query.limit
|
query.limit
|
||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
let search_pattern = query
|
|
||||||
.search
|
|
||||||
.as_deref()
|
|
||||||
.map(str::trim)
|
|
||||||
.filter(|value| !value.is_empty())
|
|
||||||
.map(|value| format!("%{}%", value.to_ascii_lowercase()));
|
|
||||||
let order_by = match query.order {
|
let order_by = match query.order {
|
||||||
ProviderCatalogKeyListOrder::Name => "internal_priority ASC, name ASC, id ASC",
|
ProviderCatalogKeyListOrder::Name => "internal_priority ASC, name ASC, id ASC",
|
||||||
ProviderCatalogKeyListOrder::CreatedAt => {
|
ProviderCatalogKeyListOrder::CreatedAt => {
|
||||||
@@ -585,99 +554,25 @@ ORDER BY provider_priority ASC, name ASC
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let count_row = sqlx::query(
|
let mut count_builder = QueryBuilder::<Postgres>::new(
|
||||||
r#"
|
"SELECT COUNT(*)::BIGINT AS total FROM provider_api_keys",
|
||||||
SELECT COUNT(*)::BIGINT AS total
|
);
|
||||||
FROM provider_api_keys
|
let mut count_where = WhereClause::new();
|
||||||
WHERE provider_id = $1
|
apply_key_page_filters(&mut count_builder, &mut count_where, query);
|
||||||
AND ($2::TEXT IS NULL OR LOWER(name) LIKE $2 OR LOWER(id) LIKE $2)
|
let total = count_builder
|
||||||
AND ($3::BOOLEAN IS NULL OR is_active = $3)
|
.build_query_scalar::<i64>()
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.bind(&query.provider_id)
|
|
||||||
.bind(search_pattern.as_deref())
|
|
||||||
.bind(query.is_active)
|
|
||||||
.fetch_one(&self.pool)
|
.fetch_one(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_postgres_err()?;
|
.map_postgres_err()?
|
||||||
let total = row_get::<i64>(&count_row, "total")?.max(0) as usize;
|
.max(0) as usize;
|
||||||
|
|
||||||
let sql = format!(
|
let mut list_builder =
|
||||||
r#"
|
QueryBuilder::<Postgres>::new(select_prefix_for_in(LIST_KEYS_BY_IDS_PREFIX));
|
||||||
SELECT
|
let mut list_where = WhereClause::new();
|
||||||
id,
|
apply_key_page_filters(&mut list_builder, &mut list_where, query);
|
||||||
provider_id,
|
list_builder.push(" ORDER BY ").push(order_by);
|
||||||
name,
|
push_limit_offset(&mut list_builder, limit, offset);
|
||||||
auth_type,
|
let items = collect_query_rows(list_builder.build().fetch(&self.pool), map_key_row).await?;
|
||||||
capabilities,
|
|
||||||
is_active,
|
|
||||||
api_formats,
|
|
||||||
auth_type_by_format,
|
|
||||||
allow_auth_channel_mismatch_formats,
|
|
||||||
COALESCE(api_key, encrypted_key) AS api_key,
|
|
||||||
auth_config,
|
|
||||||
note,
|
|
||||||
internal_priority,
|
|
||||||
rate_multipliers,
|
|
||||||
global_priority_by_format,
|
|
||||||
allowed_models,
|
|
||||||
EXTRACT(EPOCH FROM expires_at)::bigint AS expires_at_unix_secs,
|
|
||||||
cache_ttl_minutes,
|
|
||||||
max_probe_interval_minutes,
|
|
||||||
proxy,
|
|
||||||
fingerprint,
|
|
||||||
rpm_limit,
|
|
||||||
concurrent_limit,
|
|
||||||
learned_rpm_limit,
|
|
||||||
concurrent_429_count,
|
|
||||||
rpm_429_count,
|
|
||||||
EXTRACT(EPOCH FROM last_429_at)::bigint AS last_429_at_unix_secs,
|
|
||||||
last_429_type,
|
|
||||||
adjustment_history,
|
|
||||||
utilization_samples,
|
|
||||||
EXTRACT(EPOCH FROM last_probe_increase_at)::bigint AS last_probe_increase_at_unix_secs,
|
|
||||||
last_rpm_peak,
|
|
||||||
request_count,
|
|
||||||
total_tokens,
|
|
||||||
CAST(total_cost_usd AS DOUBLE PRECISION) AS total_cost_usd,
|
|
||||||
success_count,
|
|
||||||
error_count,
|
|
||||||
total_response_time_ms,
|
|
||||||
EXTRACT(EPOCH FROM last_used_at)::bigint AS last_used_at_unix_secs,
|
|
||||||
auto_fetch_models,
|
|
||||||
EXTRACT(EPOCH FROM last_models_fetch_at)::bigint AS last_models_fetch_at_unix_secs,
|
|
||||||
last_models_fetch_error,
|
|
||||||
locked_models,
|
|
||||||
model_include_patterns,
|
|
||||||
model_exclude_patterns,
|
|
||||||
upstream_metadata,
|
|
||||||
EXTRACT(EPOCH FROM oauth_invalid_at)::bigint AS oauth_invalid_at_unix_secs,
|
|
||||||
oauth_invalid_reason,
|
|
||||||
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 = $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 {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,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(StoredProviderCatalogKeyPage { items, total })
|
Ok(StoredProviderCatalogKeyPage { items, total })
|
||||||
}
|
}
|
||||||
@@ -2150,16 +2045,56 @@ fn build_list_query<'a>(
|
|||||||
ids: &'a [String],
|
ids: &'a [String],
|
||||||
suffix: &'static str,
|
suffix: &'static str,
|
||||||
) -> QueryBuilder<'a, Postgres> {
|
) -> QueryBuilder<'a, Postgres> {
|
||||||
let mut builder = QueryBuilder::<Postgres>::new(prefix);
|
let mut builder = QueryBuilder::<Postgres>::new(select_prefix_for_in(prefix));
|
||||||
let mut separated = builder.separated(", ");
|
let mut where_clause = WhereClause::new();
|
||||||
for id in ids {
|
push_in(
|
||||||
separated.push_bind(id);
|
&mut builder,
|
||||||
}
|
&mut where_clause,
|
||||||
separated.push_unseparated(")");
|
in_column_for_prefix(prefix),
|
||||||
|
ids,
|
||||||
|
);
|
||||||
builder.push(suffix);
|
builder.push(suffix);
|
||||||
builder
|
builder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn select_prefix_for_in(prefix: &'static str) -> &'static str {
|
||||||
|
prefix
|
||||||
|
.rsplit_once("\nWHERE ")
|
||||||
|
.map(|(select_prefix, _)| select_prefix)
|
||||||
|
.expect("provider catalog IN query prefix must contain WHERE")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn in_column_for_prefix(prefix: &'static str) -> &'static str {
|
||||||
|
prefix
|
||||||
|
.rsplit_once("\nWHERE ")
|
||||||
|
.and_then(|(_, predicate)| predicate.trim().strip_suffix("IN ("))
|
||||||
|
.map(str::trim)
|
||||||
|
.expect("provider catalog IN query prefix must end with IN (")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_key_page_filters<'a>(
|
||||||
|
builder: &mut QueryBuilder<'a, Postgres>,
|
||||||
|
where_clause: &mut WhereClause,
|
||||||
|
query: &'a ProviderCatalogKeyListQuery,
|
||||||
|
) {
|
||||||
|
push_eq(
|
||||||
|
builder,
|
||||||
|
where_clause,
|
||||||
|
"provider_id",
|
||||||
|
query.provider_id.clone(),
|
||||||
|
);
|
||||||
|
if let Some(search) = query.search.as_deref() {
|
||||||
|
push_ci_contains_any(
|
||||||
|
builder,
|
||||||
|
where_clause,
|
||||||
|
SqlDialect::Postgres,
|
||||||
|
&["name", "id"],
|
||||||
|
search,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
push_optional_eq(builder, where_clause, "is_active", query.is_active);
|
||||||
|
}
|
||||||
|
|
||||||
fn row_get<T>(row: &PgRow, column: &str) -> Result<T, DataLayerError>
|
fn row_get<T>(row: &PgRow, column: &str) -> Result<T, DataLayerError>
|
||||||
where
|
where
|
||||||
for<'r> T: sqlx::Decode<'r, sqlx::Postgres> + sqlx::Type<sqlx::Postgres>,
|
for<'r> T: sqlx::Decode<'r, sqlx::Postgres> + sqlx::Type<sqlx::Postgres>,
|
||||||
@@ -2623,8 +2558,9 @@ mod tests {
|
|||||||
"auth_type_by_format,\n allow_auth_channel_mismatch_formats,\n COALESCE(api_key, encrypted_key) AS api_key",
|
"auth_type_by_format,\n allow_auth_channel_mismatch_formats,\n COALESCE(api_key, encrypted_key) AS api_key",
|
||||||
)
|
)
|
||||||
.count()
|
.count()
|
||||||
>= 3
|
>= 2
|
||||||
);
|
);
|
||||||
|
assert!(source.contains("QueryBuilder::<Postgres>::new(select_prefix_for_in("));
|
||||||
assert!(source.contains(".bind(&key.allow_auth_channel_mismatch_formats)"));
|
assert!(source.contains(".bind(&key.allow_auth_channel_mismatch_formats)"));
|
||||||
assert!(source.contains("row.try_get(\"allow_auth_channel_mismatch_formats\").ok()"));
|
assert!(source.contains("row.try_get(\"allow_auth_channel_mismatch_formats\").ok()"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,282 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use sqlx::{sqlite::SqliteRow, Row};
|
use sqlx::{sqlite::SqliteRow, QueryBuilder, Row, Sqlite};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
InMemoryProviderCatalogReadRepository, ProviderCatalogKeyListQuery,
|
ProviderCatalogKeyListOrder, ProviderCatalogKeyListQuery, ProviderCatalogReadRepository,
|
||||||
ProviderCatalogReadRepository, ProviderCatalogWriteRepository, StoredProviderCatalogEndpoint,
|
ProviderCatalogWriteRepository, StoredProviderCatalogEndpoint, StoredProviderCatalogKey,
|
||||||
StoredProviderCatalogKey, StoredProviderCatalogKeyPage, StoredProviderCatalogKeyStats,
|
StoredProviderCatalogKeyPage, StoredProviderCatalogKeyStats, StoredProviderCatalogProvider,
|
||||||
StoredProviderCatalogProvider,
|
|
||||||
};
|
};
|
||||||
use crate::driver::sqlite::{sqlite_optional_real, SqlitePool};
|
use crate::driver::sqlite::{sqlite_optional_real, SqlitePool};
|
||||||
use crate::error::SqlResultExt;
|
use crate::error::SqlResultExt;
|
||||||
use crate::DataLayerError;
|
use crate::DataLayerError;
|
||||||
|
use aether_data_query::{
|
||||||
|
push_ci_contains_any, push_eq, push_in, push_limit_offset, push_optional_eq, SqlDialect,
|
||||||
|
WhereClause,
|
||||||
|
};
|
||||||
|
|
||||||
|
const LIST_PROVIDERS_BY_IDS_PREFIX: &str = r#"
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
website,
|
||||||
|
provider_type,
|
||||||
|
billing_type,
|
||||||
|
CAST(monthly_quota_usd AS REAL) AS monthly_quota_usd,
|
||||||
|
CAST(monthly_used_usd AS REAL) 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,
|
||||||
|
provider_priority,
|
||||||
|
is_active,
|
||||||
|
keep_priority_on_conversion,
|
||||||
|
enable_format_conversion,
|
||||||
|
concurrent_limit,
|
||||||
|
max_retries,
|
||||||
|
proxy,
|
||||||
|
request_timeout,
|
||||||
|
stream_first_byte_timeout,
|
||||||
|
config,
|
||||||
|
created_at AS created_at_unix_ms,
|
||||||
|
updated_at AS updated_at_unix_secs
|
||||||
|
FROM providers
|
||||||
|
WHERE id IN (
|
||||||
|
"#;
|
||||||
|
|
||||||
|
const LIST_ENDPOINTS_BY_IDS_PREFIX: &str = r#"
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
provider_id,
|
||||||
|
api_format,
|
||||||
|
api_family,
|
||||||
|
endpoint_kind,
|
||||||
|
is_active,
|
||||||
|
health_score,
|
||||||
|
base_url,
|
||||||
|
header_rules,
|
||||||
|
body_rules,
|
||||||
|
max_retries,
|
||||||
|
custom_path,
|
||||||
|
config,
|
||||||
|
format_acceptance_config,
|
||||||
|
proxy,
|
||||||
|
created_at AS created_at_unix_ms,
|
||||||
|
updated_at AS updated_at_unix_secs
|
||||||
|
FROM provider_endpoints
|
||||||
|
WHERE id IN (
|
||||||
|
"#;
|
||||||
|
|
||||||
|
const LIST_ENDPOINTS_BY_PROVIDER_IDS_PREFIX: &str = r#"
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
provider_id,
|
||||||
|
api_format,
|
||||||
|
api_family,
|
||||||
|
endpoint_kind,
|
||||||
|
is_active,
|
||||||
|
health_score,
|
||||||
|
base_url,
|
||||||
|
header_rules,
|
||||||
|
body_rules,
|
||||||
|
max_retries,
|
||||||
|
custom_path,
|
||||||
|
config,
|
||||||
|
format_acceptance_config,
|
||||||
|
proxy,
|
||||||
|
created_at AS created_at_unix_ms,
|
||||||
|
updated_at AS updated_at_unix_secs
|
||||||
|
FROM provider_endpoints
|
||||||
|
WHERE provider_id IN (
|
||||||
|
"#;
|
||||||
|
|
||||||
|
const LIST_KEYS_BY_IDS_PREFIX: &str = r#"
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
provider_id,
|
||||||
|
name,
|
||||||
|
auth_type,
|
||||||
|
capabilities,
|
||||||
|
is_active,
|
||||||
|
api_formats,
|
||||||
|
auth_type_by_format,
|
||||||
|
allow_auth_channel_mismatch_formats,
|
||||||
|
COALESCE(api_key, encrypted_key) AS api_key,
|
||||||
|
auth_config,
|
||||||
|
note,
|
||||||
|
internal_priority,
|
||||||
|
rate_multipliers,
|
||||||
|
global_priority_by_format,
|
||||||
|
allowed_models,
|
||||||
|
expires_at AS expires_at_unix_secs,
|
||||||
|
cache_ttl_minutes,
|
||||||
|
max_probe_interval_minutes,
|
||||||
|
proxy,
|
||||||
|
fingerprint,
|
||||||
|
rpm_limit,
|
||||||
|
concurrent_limit,
|
||||||
|
learned_rpm_limit,
|
||||||
|
concurrent_429_count,
|
||||||
|
rpm_429_count,
|
||||||
|
last_429_at AS last_429_at_unix_secs,
|
||||||
|
last_429_type,
|
||||||
|
adjustment_history,
|
||||||
|
utilization_samples,
|
||||||
|
last_probe_increase_at AS last_probe_increase_at_unix_secs,
|
||||||
|
last_rpm_peak,
|
||||||
|
request_count,
|
||||||
|
total_tokens,
|
||||||
|
CAST(total_cost_usd AS REAL) AS total_cost_usd,
|
||||||
|
success_count,
|
||||||
|
error_count,
|
||||||
|
total_response_time_ms,
|
||||||
|
last_used_at AS last_used_at_unix_secs,
|
||||||
|
auto_fetch_models,
|
||||||
|
last_models_fetch_at AS last_models_fetch_at_unix_secs,
|
||||||
|
last_models_fetch_error,
|
||||||
|
locked_models,
|
||||||
|
model_include_patterns,
|
||||||
|
model_exclude_patterns,
|
||||||
|
upstream_metadata,
|
||||||
|
oauth_invalid_at AS oauth_invalid_at_unix_secs,
|
||||||
|
oauth_invalid_reason,
|
||||||
|
status_snapshot,
|
||||||
|
created_at AS created_at_unix_ms,
|
||||||
|
updated_at AS updated_at_unix_secs,
|
||||||
|
health_by_format,
|
||||||
|
circuit_breaker_by_format
|
||||||
|
FROM provider_api_keys
|
||||||
|
WHERE id IN (
|
||||||
|
"#;
|
||||||
|
|
||||||
|
const LIST_KEYS_BY_PROVIDER_IDS_PREFIX: &str = r#"
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
provider_id,
|
||||||
|
name,
|
||||||
|
auth_type,
|
||||||
|
capabilities,
|
||||||
|
is_active,
|
||||||
|
api_formats,
|
||||||
|
auth_type_by_format,
|
||||||
|
allow_auth_channel_mismatch_formats,
|
||||||
|
COALESCE(api_key, encrypted_key) AS api_key,
|
||||||
|
auth_config,
|
||||||
|
note,
|
||||||
|
internal_priority,
|
||||||
|
rate_multipliers,
|
||||||
|
global_priority_by_format,
|
||||||
|
allowed_models,
|
||||||
|
expires_at AS expires_at_unix_secs,
|
||||||
|
cache_ttl_minutes,
|
||||||
|
max_probe_interval_minutes,
|
||||||
|
proxy,
|
||||||
|
fingerprint,
|
||||||
|
rpm_limit,
|
||||||
|
concurrent_limit,
|
||||||
|
learned_rpm_limit,
|
||||||
|
concurrent_429_count,
|
||||||
|
rpm_429_count,
|
||||||
|
last_429_at AS last_429_at_unix_secs,
|
||||||
|
last_429_type,
|
||||||
|
adjustment_history,
|
||||||
|
utilization_samples,
|
||||||
|
last_probe_increase_at AS last_probe_increase_at_unix_secs,
|
||||||
|
last_rpm_peak,
|
||||||
|
request_count,
|
||||||
|
total_tokens,
|
||||||
|
CAST(total_cost_usd AS REAL) AS total_cost_usd,
|
||||||
|
success_count,
|
||||||
|
error_count,
|
||||||
|
total_response_time_ms,
|
||||||
|
last_used_at AS last_used_at_unix_secs,
|
||||||
|
auto_fetch_models,
|
||||||
|
last_models_fetch_at AS last_models_fetch_at_unix_secs,
|
||||||
|
last_models_fetch_error,
|
||||||
|
locked_models,
|
||||||
|
model_include_patterns,
|
||||||
|
model_exclude_patterns,
|
||||||
|
upstream_metadata,
|
||||||
|
oauth_invalid_at AS oauth_invalid_at_unix_secs,
|
||||||
|
oauth_invalid_reason,
|
||||||
|
status_snapshot,
|
||||||
|
created_at AS created_at_unix_ms,
|
||||||
|
updated_at AS updated_at_unix_secs,
|
||||||
|
health_by_format,
|
||||||
|
circuit_breaker_by_format
|
||||||
|
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,
|
||||||
|
NULL AS capabilities,
|
||||||
|
is_active,
|
||||||
|
api_formats,
|
||||||
|
NULL AS auth_type_by_format,
|
||||||
|
NULL AS allow_auth_channel_mismatch_formats,
|
||||||
|
'summary' AS api_key,
|
||||||
|
CASE
|
||||||
|
WHEN auth_config IS NULL THEN NULL
|
||||||
|
ELSE '{}'
|
||||||
|
END AS auth_config,
|
||||||
|
NULL AS note,
|
||||||
|
NULL AS internal_priority,
|
||||||
|
NULL AS rate_multipliers,
|
||||||
|
NULL AS global_priority_by_format,
|
||||||
|
NULL AS allowed_models,
|
||||||
|
NULL AS expires_at_unix_secs,
|
||||||
|
NULL AS cache_ttl_minutes,
|
||||||
|
NULL AS max_probe_interval_minutes,
|
||||||
|
NULL AS proxy,
|
||||||
|
NULL AS fingerprint,
|
||||||
|
NULL AS rpm_limit,
|
||||||
|
NULL AS concurrent_limit,
|
||||||
|
NULL AS learned_rpm_limit,
|
||||||
|
NULL AS concurrent_429_count,
|
||||||
|
NULL AS rpm_429_count,
|
||||||
|
NULL AS last_429_at_unix_secs,
|
||||||
|
NULL AS last_429_type,
|
||||||
|
NULL AS adjustment_history,
|
||||||
|
NULL AS utilization_samples,
|
||||||
|
NULL AS last_probe_increase_at_unix_secs,
|
||||||
|
NULL AS last_rpm_peak,
|
||||||
|
NULL AS request_count,
|
||||||
|
0 AS total_tokens,
|
||||||
|
0.0 AS total_cost_usd,
|
||||||
|
NULL AS success_count,
|
||||||
|
NULL AS error_count,
|
||||||
|
NULL AS total_response_time_ms,
|
||||||
|
NULL AS last_used_at_unix_secs,
|
||||||
|
FALSE AS auto_fetch_models,
|
||||||
|
NULL AS last_models_fetch_at_unix_secs,
|
||||||
|
NULL AS last_models_fetch_error,
|
||||||
|
NULL AS locked_models,
|
||||||
|
NULL AS model_include_patterns,
|
||||||
|
NULL AS model_exclude_patterns,
|
||||||
|
NULL AS upstream_metadata,
|
||||||
|
NULL AS oauth_invalid_at_unix_secs,
|
||||||
|
NULL AS oauth_invalid_reason,
|
||||||
|
NULL AS status_snapshot,
|
||||||
|
NULL AS created_at_unix_ms,
|
||||||
|
NULL AS updated_at_unix_secs,
|
||||||
|
health_by_format,
|
||||||
|
NULL AS 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,
|
||||||
|
COUNT(*) AS total_keys,
|
||||||
|
SUM(CASE WHEN is_active THEN 1 ELSE 0 END) AS active_keys
|
||||||
|
FROM provider_api_keys
|
||||||
|
WHERE provider_id IN (
|
||||||
|
"#;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct SqliteProviderCatalogReadRepository {
|
pub struct SqliteProviderCatalogReadRepository {
|
||||||
@@ -21,92 +288,232 @@ impl SqliteProviderCatalogReadRepository {
|
|||||||
Self { pool }
|
Self { pool }
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn load_memory(&self) -> Result<InMemoryProviderCatalogReadRepository, DataLayerError> {
|
pub async fn list_providers_by_ids(
|
||||||
Ok(InMemoryProviderCatalogReadRepository::seed(
|
&self,
|
||||||
self.load_providers().await?,
|
provider_ids: &[String],
|
||||||
self.load_endpoints().await?,
|
) -> Result<Vec<StoredProviderCatalogProvider>, DataLayerError> {
|
||||||
self.load_keys().await?,
|
if provider_ids.is_empty() {
|
||||||
))
|
return Ok(Vec::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn load_providers(&self) -> Result<Vec<StoredProviderCatalogProvider>, DataLayerError> {
|
let rows = build_list_query(
|
||||||
let rows = sqlx::query(
|
LIST_PROVIDERS_BY_IDS_PREFIX,
|
||||||
r#"
|
provider_ids,
|
||||||
SELECT
|
" ORDER BY name ASC",
|
||||||
id, name, description, website, provider_type, billing_type,
|
|
||||||
CAST(monthly_quota_usd AS REAL) AS monthly_quota_usd,
|
|
||||||
CAST(monthly_used_usd AS REAL) 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,
|
|
||||||
provider_priority, is_active, keep_priority_on_conversion,
|
|
||||||
enable_format_conversion, concurrent_limit, max_retries, proxy,
|
|
||||||
request_timeout, stream_first_byte_timeout, config,
|
|
||||||
created_at AS created_at_unix_ms,
|
|
||||||
updated_at AS updated_at_unix_secs
|
|
||||||
FROM providers
|
|
||||||
"#,
|
|
||||||
)
|
)
|
||||||
|
.build()
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_sql_err()?;
|
.map_sql_err()?;
|
||||||
rows.iter().map(map_provider_row).collect()
|
rows.iter().map(map_provider_row).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn load_endpoints(&self) -> Result<Vec<StoredProviderCatalogEndpoint>, DataLayerError> {
|
pub async fn list_providers(
|
||||||
let rows = sqlx::query(
|
&self,
|
||||||
r#"
|
active_only: bool,
|
||||||
SELECT
|
) -> Result<Vec<StoredProviderCatalogProvider>, DataLayerError> {
|
||||||
id, provider_id, api_format, api_family, endpoint_kind, is_active,
|
let mut builder =
|
||||||
health_score, base_url, header_rules, body_rules, max_retries,
|
QueryBuilder::<Sqlite>::new(select_prefix_for_in(LIST_PROVIDERS_BY_IDS_PREFIX));
|
||||||
custom_path, config, format_acceptance_config, proxy,
|
let mut where_clause = WhereClause::new();
|
||||||
created_at AS created_at_unix_ms,
|
if active_only {
|
||||||
updated_at AS updated_at_unix_secs
|
push_eq(&mut builder, &mut where_clause, "is_active", true);
|
||||||
FROM provider_endpoints
|
}
|
||||||
WHERE api_format IS NOT NULL
|
builder.push(" ORDER BY provider_priority ASC, name ASC");
|
||||||
"#,
|
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
||||||
|
rows.iter().map(map_provider_row).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_endpoints_by_ids(
|
||||||
|
&self,
|
||||||
|
endpoint_ids: &[String],
|
||||||
|
) -> Result<Vec<StoredProviderCatalogEndpoint>, DataLayerError> {
|
||||||
|
if endpoint_ids.is_empty() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let rows = build_list_query(
|
||||||
|
LIST_ENDPOINTS_BY_IDS_PREFIX,
|
||||||
|
endpoint_ids,
|
||||||
|
" ORDER BY api_format ASC, id ASC",
|
||||||
)
|
)
|
||||||
|
.build()
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_sql_err()?;
|
.map_sql_err()?;
|
||||||
rows.iter().map(map_endpoint_row).collect()
|
rows.iter().map(map_endpoint_row).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn load_keys(&self) -> Result<Vec<StoredProviderCatalogKey>, DataLayerError> {
|
pub async fn list_endpoints_by_provider_ids(
|
||||||
let rows = sqlx::query(
|
&self,
|
||||||
r#"
|
provider_ids: &[String],
|
||||||
SELECT
|
) -> Result<Vec<StoredProviderCatalogEndpoint>, DataLayerError> {
|
||||||
id, provider_id, name, auth_type, capabilities, is_active, api_formats,
|
if provider_ids.is_empty() {
|
||||||
auth_type_by_format, allow_auth_channel_mismatch_formats,
|
return Ok(Vec::new());
|
||||||
COALESCE(api_key, encrypted_key) AS api_key,
|
}
|
||||||
auth_config, note, internal_priority, rate_multipliers,
|
|
||||||
global_priority_by_format, allowed_models,
|
let rows = build_list_query(
|
||||||
expires_at AS expires_at_unix_secs,
|
LIST_ENDPOINTS_BY_PROVIDER_IDS_PREFIX,
|
||||||
cache_ttl_minutes, max_probe_interval_minutes, proxy, fingerprint,
|
provider_ids,
|
||||||
rpm_limit, concurrent_limit, learned_rpm_limit, concurrent_429_count,
|
" ORDER BY provider_id ASC, api_format ASC, id ASC",
|
||||||
rpm_429_count, last_429_at AS last_429_at_unix_secs, last_429_type,
|
|
||||||
adjustment_history, utilization_samples,
|
|
||||||
last_probe_increase_at AS last_probe_increase_at_unix_secs,
|
|
||||||
last_rpm_peak, request_count, total_tokens, total_cost_usd,
|
|
||||||
success_count, error_count, total_response_time_ms,
|
|
||||||
last_used_at AS last_used_at_unix_secs, auto_fetch_models,
|
|
||||||
last_models_fetch_at AS last_models_fetch_at_unix_secs,
|
|
||||||
last_models_fetch_error, locked_models, model_include_patterns,
|
|
||||||
model_exclude_patterns, upstream_metadata,
|
|
||||||
oauth_invalid_at AS oauth_invalid_at_unix_secs,
|
|
||||||
oauth_invalid_reason, status_snapshot,
|
|
||||||
created_at AS created_at_unix_ms,
|
|
||||||
updated_at AS updated_at_unix_secs,
|
|
||||||
health_by_format, circuit_breaker_by_format
|
|
||||||
FROM provider_api_keys
|
|
||||||
"#,
|
|
||||||
)
|
)
|
||||||
|
.build()
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_sql_err()?;
|
||||||
|
rows.iter().map(map_endpoint_row).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_keys_by_ids(
|
||||||
|
&self,
|
||||||
|
key_ids: &[String],
|
||||||
|
) -> Result<Vec<StoredProviderCatalogKey>, DataLayerError> {
|
||||||
|
if key_ids.is_empty() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let rows = build_list_query(
|
||||||
|
LIST_KEYS_BY_IDS_PREFIX,
|
||||||
|
key_ids,
|
||||||
|
" ORDER BY name ASC, id ASC",
|
||||||
|
)
|
||||||
|
.build()
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_sql_err()?;
|
.map_sql_err()?;
|
||||||
rows.iter().map(map_key_row).collect()
|
rows.iter().map(map_key_row).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn list_keys_by_provider_ids(
|
||||||
|
&self,
|
||||||
|
provider_ids: &[String],
|
||||||
|
) -> Result<Vec<StoredProviderCatalogKey>, DataLayerError> {
|
||||||
|
if provider_ids.is_empty() {
|
||||||
|
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",
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_sql_err()?;
|
||||||
|
rows.iter().map(map_key_row).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
let rows = build_list_query(
|
||||||
|
LIST_KEY_SUMMARIES_BY_PROVIDER_IDS_PREFIX,
|
||||||
|
provider_ids,
|
||||||
|
" ORDER BY provider_id ASC, id ASC",
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_sql_err()?;
|
||||||
|
rows.iter().map(map_key_row).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_keys_page(
|
||||||
|
&self,
|
||||||
|
query: &ProviderCatalogKeyListQuery,
|
||||||
|
) -> Result<StoredProviderCatalogKeyPage, DataLayerError> {
|
||||||
|
if query.provider_id.trim().is_empty() {
|
||||||
|
return Err(DataLayerError::InvalidInput(
|
||||||
|
"provider catalog provider_id is empty".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let offset = i64::try_from(query.offset).map_err(|_| {
|
||||||
|
DataLayerError::InvalidInput(format!(
|
||||||
|
"invalid provider catalog key offset: {}",
|
||||||
|
query.offset
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
let limit = i64::try_from(query.limit).map_err(|_| {
|
||||||
|
DataLayerError::InvalidInput(format!(
|
||||||
|
"invalid provider catalog key limit: {}",
|
||||||
|
query.limit
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
let order_by = match query.order {
|
||||||
|
ProviderCatalogKeyListOrder::Name => "internal_priority ASC, name ASC, id ASC",
|
||||||
|
ProviderCatalogKeyListOrder::CreatedAt => {
|
||||||
|
"internal_priority ASC, COALESCE(created_at, 0) ASC, id ASC"
|
||||||
|
}
|
||||||
|
ProviderCatalogKeyListOrder::CreatedAtAsc => {
|
||||||
|
"created_at IS NULL ASC, created_at ASC, name ASC, id ASC"
|
||||||
|
}
|
||||||
|
ProviderCatalogKeyListOrder::CreatedAtDesc => {
|
||||||
|
"created_at IS NULL ASC, created_at DESC, name ASC, id ASC"
|
||||||
|
}
|
||||||
|
ProviderCatalogKeyListOrder::LastUsedAtAsc => {
|
||||||
|
"last_used_at IS NULL ASC, last_used_at ASC, name ASC, id ASC"
|
||||||
|
}
|
||||||
|
ProviderCatalogKeyListOrder::LastUsedAtDesc => {
|
||||||
|
"last_used_at IS NULL ASC, last_used_at DESC, name ASC, id ASC"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut count_builder =
|
||||||
|
QueryBuilder::<Sqlite>::new("SELECT COUNT(*) AS total FROM provider_api_keys");
|
||||||
|
let mut count_where = WhereClause::new();
|
||||||
|
apply_key_page_filters(&mut count_builder, &mut count_where, query);
|
||||||
|
let total = count_builder
|
||||||
|
.build_query_scalar::<i64>()
|
||||||
|
.fetch_one(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_sql_err()?
|
||||||
|
.max(0) as usize;
|
||||||
|
|
||||||
|
let mut list_builder =
|
||||||
|
QueryBuilder::<Sqlite>::new(select_prefix_for_in(LIST_KEYS_BY_IDS_PREFIX));
|
||||||
|
let mut list_where = WhereClause::new();
|
||||||
|
apply_key_page_filters(&mut list_builder, &mut list_where, query);
|
||||||
|
list_builder.push(" ORDER BY ").push(order_by);
|
||||||
|
push_limit_offset(&mut list_builder, limit, offset);
|
||||||
|
let rows = list_builder
|
||||||
|
.build()
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_sql_err()?;
|
||||||
|
let items = rows
|
||||||
|
.iter()
|
||||||
|
.map(map_key_row)
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
|
||||||
|
Ok(StoredProviderCatalogKeyPage { items, total })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_key_stats_by_provider_ids(
|
||||||
|
&self,
|
||||||
|
provider_ids: &[String],
|
||||||
|
) -> Result<Vec<StoredProviderCatalogKeyStats>, DataLayerError> {
|
||||||
|
if provider_ids.is_empty() {
|
||||||
|
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",
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_sql_err()?;
|
||||||
|
rows.iter().map(map_key_stats_row).collect()
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn create_provider(
|
pub async fn create_provider(
|
||||||
&self,
|
&self,
|
||||||
provider: &StoredProviderCatalogProvider,
|
provider: &StoredProviderCatalogProvider,
|
||||||
@@ -865,81 +1272,63 @@ impl ProviderCatalogReadRepository for SqliteProviderCatalogReadRepository {
|
|||||||
&self,
|
&self,
|
||||||
active_only: bool,
|
active_only: bool,
|
||||||
) -> Result<Vec<StoredProviderCatalogProvider>, DataLayerError> {
|
) -> Result<Vec<StoredProviderCatalogProvider>, DataLayerError> {
|
||||||
self.load_memory().await?.list_providers(active_only).await
|
Self::list_providers(self, active_only).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_providers_by_ids(
|
async fn list_providers_by_ids(
|
||||||
&self,
|
&self,
|
||||||
provider_ids: &[String],
|
provider_ids: &[String],
|
||||||
) -> Result<Vec<StoredProviderCatalogProvider>, DataLayerError> {
|
) -> Result<Vec<StoredProviderCatalogProvider>, DataLayerError> {
|
||||||
self.load_memory()
|
Self::list_providers_by_ids(self, provider_ids).await
|
||||||
.await?
|
|
||||||
.list_providers_by_ids(provider_ids)
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_endpoints_by_ids(
|
async fn list_endpoints_by_ids(
|
||||||
&self,
|
&self,
|
||||||
endpoint_ids: &[String],
|
endpoint_ids: &[String],
|
||||||
) -> Result<Vec<StoredProviderCatalogEndpoint>, DataLayerError> {
|
) -> Result<Vec<StoredProviderCatalogEndpoint>, DataLayerError> {
|
||||||
self.load_memory()
|
Self::list_endpoints_by_ids(self, endpoint_ids).await
|
||||||
.await?
|
|
||||||
.list_endpoints_by_ids(endpoint_ids)
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_endpoints_by_provider_ids(
|
async fn list_endpoints_by_provider_ids(
|
||||||
&self,
|
&self,
|
||||||
provider_ids: &[String],
|
provider_ids: &[String],
|
||||||
) -> Result<Vec<StoredProviderCatalogEndpoint>, DataLayerError> {
|
) -> Result<Vec<StoredProviderCatalogEndpoint>, DataLayerError> {
|
||||||
self.load_memory()
|
Self::list_endpoints_by_provider_ids(self, provider_ids).await
|
||||||
.await?
|
|
||||||
.list_endpoints_by_provider_ids(provider_ids)
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_keys_by_ids(
|
async fn list_keys_by_ids(
|
||||||
&self,
|
&self,
|
||||||
key_ids: &[String],
|
key_ids: &[String],
|
||||||
) -> Result<Vec<StoredProviderCatalogKey>, DataLayerError> {
|
) -> Result<Vec<StoredProviderCatalogKey>, DataLayerError> {
|
||||||
self.load_memory().await?.list_keys_by_ids(key_ids).await
|
Self::list_keys_by_ids(self, key_ids).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_keys_by_provider_ids(
|
async fn list_keys_by_provider_ids(
|
||||||
&self,
|
&self,
|
||||||
provider_ids: &[String],
|
provider_ids: &[String],
|
||||||
) -> Result<Vec<StoredProviderCatalogKey>, DataLayerError> {
|
) -> Result<Vec<StoredProviderCatalogKey>, DataLayerError> {
|
||||||
self.load_memory()
|
Self::list_keys_by_provider_ids(self, provider_ids).await
|
||||||
.await?
|
|
||||||
.list_keys_by_provider_ids(provider_ids)
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_key_summaries_by_provider_ids(
|
async fn list_key_summaries_by_provider_ids(
|
||||||
&self,
|
&self,
|
||||||
provider_ids: &[String],
|
provider_ids: &[String],
|
||||||
) -> Result<Vec<StoredProviderCatalogKey>, DataLayerError> {
|
) -> Result<Vec<StoredProviderCatalogKey>, DataLayerError> {
|
||||||
self.load_memory()
|
Self::list_key_summaries_by_provider_ids(self, provider_ids).await
|
||||||
.await?
|
|
||||||
.list_key_summaries_by_provider_ids(provider_ids)
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_keys_page(
|
async fn list_keys_page(
|
||||||
&self,
|
&self,
|
||||||
query: &ProviderCatalogKeyListQuery,
|
query: &ProviderCatalogKeyListQuery,
|
||||||
) -> Result<StoredProviderCatalogKeyPage, DataLayerError> {
|
) -> Result<StoredProviderCatalogKeyPage, DataLayerError> {
|
||||||
self.load_memory().await?.list_keys_page(query).await
|
Self::list_keys_page(self, query).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_key_stats_by_provider_ids(
|
async fn list_key_stats_by_provider_ids(
|
||||||
&self,
|
&self,
|
||||||
provider_ids: &[String],
|
provider_ids: &[String],
|
||||||
) -> Result<Vec<StoredProviderCatalogKeyStats>, DataLayerError> {
|
) -> Result<Vec<StoredProviderCatalogKeyStats>, DataLayerError> {
|
||||||
self.load_memory()
|
Self::list_key_stats_by_provider_ids(self, provider_ids).await
|
||||||
.await?
|
|
||||||
.list_key_stats_by_provider_ids(provider_ids)
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1058,6 +1447,61 @@ impl ProviderCatalogWriteRepository for SqliteProviderCatalogReadRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_list_query<'a>(
|
||||||
|
prefix: &'static str,
|
||||||
|
ids: &'a [String],
|
||||||
|
suffix: &'static str,
|
||||||
|
) -> QueryBuilder<'a, Sqlite> {
|
||||||
|
let mut builder = QueryBuilder::<Sqlite>::new(select_prefix_for_in(prefix));
|
||||||
|
let mut where_clause = WhereClause::new();
|
||||||
|
push_in(
|
||||||
|
&mut builder,
|
||||||
|
&mut where_clause,
|
||||||
|
in_column_for_prefix(prefix),
|
||||||
|
ids,
|
||||||
|
);
|
||||||
|
builder.push(suffix);
|
||||||
|
builder
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_prefix_for_in(prefix: &'static str) -> &'static str {
|
||||||
|
prefix
|
||||||
|
.rsplit_once("\nWHERE ")
|
||||||
|
.map(|(select_prefix, _)| select_prefix)
|
||||||
|
.expect("provider catalog IN query prefix must contain WHERE")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn in_column_for_prefix(prefix: &'static str) -> &'static str {
|
||||||
|
prefix
|
||||||
|
.rsplit_once("\nWHERE ")
|
||||||
|
.and_then(|(_, predicate)| predicate.trim().strip_suffix("IN ("))
|
||||||
|
.map(str::trim)
|
||||||
|
.expect("provider catalog IN query prefix must end with IN (")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_key_page_filters<'a>(
|
||||||
|
builder: &mut QueryBuilder<'a, Sqlite>,
|
||||||
|
where_clause: &mut WhereClause,
|
||||||
|
query: &'a ProviderCatalogKeyListQuery,
|
||||||
|
) {
|
||||||
|
push_eq(
|
||||||
|
builder,
|
||||||
|
where_clause,
|
||||||
|
"provider_id",
|
||||||
|
query.provider_id.clone(),
|
||||||
|
);
|
||||||
|
if let Some(search) = query.search.as_deref() {
|
||||||
|
push_ci_contains_any(
|
||||||
|
builder,
|
||||||
|
where_clause,
|
||||||
|
SqlDialect::Sqlite,
|
||||||
|
&["name", "id"],
|
||||||
|
search,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
push_optional_eq(builder, where_clause, "is_active", query.is_active);
|
||||||
|
}
|
||||||
|
|
||||||
fn current_unix_secs() -> u64 {
|
fn current_unix_secs() -> u64 {
|
||||||
chrono::Utc::now().timestamp().max(0) as u64
|
chrono::Utc::now().timestamp().max(0) as u64
|
||||||
}
|
}
|
||||||
@@ -1346,6 +1790,14 @@ fn map_endpoint_row(row: &SqliteRow) -> Result<StoredProviderCatalogEndpoint, Da
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn map_key_stats_row(row: &SqliteRow) -> Result<StoredProviderCatalogKeyStats, DataLayerError> {
|
||||||
|
StoredProviderCatalogKeyStats::new(
|
||||||
|
row.try_get("provider_id").map_sql_err()?,
|
||||||
|
row.try_get("total_keys").map_sql_err()?,
|
||||||
|
row.try_get("active_keys").map_sql_err()?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn map_key_row(row: &SqliteRow) -> Result<StoredProviderCatalogKey, DataLayerError> {
|
fn map_key_row(row: &SqliteRow) -> Result<StoredProviderCatalogKey, DataLayerError> {
|
||||||
let total_cost_usd = sqlite_optional_real(row, "total_cost_usd")?.unwrap_or(0.0);
|
let total_cost_usd = sqlite_optional_real(row, "total_cost_usd")?.unwrap_or(0.0);
|
||||||
if !total_cost_usd.is_finite() {
|
if !total_cost_usd.is_finite() {
|
||||||
@@ -1542,8 +1994,8 @@ mod tests {
|
|||||||
use super::SqliteProviderCatalogReadRepository;
|
use super::SqliteProviderCatalogReadRepository;
|
||||||
use crate::lifecycle::migrate::run_sqlite_migrations;
|
use crate::lifecycle::migrate::run_sqlite_migrations;
|
||||||
use crate::repository::provider_catalog::{
|
use crate::repository::provider_catalog::{
|
||||||
ProviderCatalogKeyListOrder, ProviderCatalogKeyListQuery, ProviderCatalogReadRepository,
|
ProviderCatalogKeyListOrder, ProviderCatalogKeyListQuery, StoredProviderCatalogEndpoint,
|
||||||
StoredProviderCatalogEndpoint, StoredProviderCatalogKey, StoredProviderCatalogProvider,
|
StoredProviderCatalogKey, StoredProviderCatalogProvider,
|
||||||
};
|
};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use sqlx::{mysql::MySqlRow, Row};
|
use sqlx::{mysql::MySqlRow, MySql, QueryBuilder, Row};
|
||||||
|
|
||||||
use super::types::{
|
use super::types::{
|
||||||
bucket_start_unix_secs, build_tunnel_error_event_detail, build_tunnel_metrics_sample,
|
bucket_start_unix_secs, build_tunnel_error_event_detail, build_tunnel_metrics_sample,
|
||||||
@@ -14,6 +14,7 @@ use super::types::{
|
|||||||
use crate::driver::mysql::MysqlPool;
|
use crate::driver::mysql::MysqlPool;
|
||||||
use crate::error::SqlResultExt;
|
use crate::error::SqlResultExt;
|
||||||
use crate::DataLayerError;
|
use crate::DataLayerError;
|
||||||
|
use aether_data_query::{push_eq, push_limit, WhereClause};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct MysqlProxyNodeReadRepository {
|
pub struct MysqlProxyNodeReadRepository {
|
||||||
@@ -337,13 +338,23 @@ SELECT
|
|||||||
FROM proxy_nodes
|
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]
|
#[async_trait]
|
||||||
impl ProxyNodeReadRepository for MysqlProxyNodeReadRepository {
|
impl ProxyNodeReadRepository for MysqlProxyNodeReadRepository {
|
||||||
async fn list_proxy_nodes(&self) -> Result<Vec<StoredProxyNode>, DataLayerError> {
|
async fn list_proxy_nodes(&self) -> Result<Vec<StoredProxyNode>, DataLayerError> {
|
||||||
let rows = sqlx::query(&format!("{PROXY_NODE_COLUMNS} ORDER BY name ASC, id ASC"))
|
let mut builder = QueryBuilder::<MySql>::new(PROXY_NODE_COLUMNS);
|
||||||
.fetch_all(&self.pool)
|
builder.push(" ORDER BY name ASC, id ASC");
|
||||||
.await
|
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
||||||
.map_sql_err()?;
|
|
||||||
rows.iter().map(map_proxy_node_row).collect()
|
rows.iter().map(map_proxy_node_row).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,8 +362,12 @@ impl ProxyNodeReadRepository for MysqlProxyNodeReadRepository {
|
|||||||
&self,
|
&self,
|
||||||
node_id: &str,
|
node_id: &str,
|
||||||
) -> Result<Option<StoredProxyNode>, DataLayerError> {
|
) -> Result<Option<StoredProxyNode>, DataLayerError> {
|
||||||
let row = sqlx::query(&format!("{PROXY_NODE_COLUMNS} WHERE id = ? LIMIT 1"))
|
let mut builder = QueryBuilder::<MySql>::new(PROXY_NODE_COLUMNS);
|
||||||
.bind(node_id)
|
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()
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_sql_err()?;
|
.map_sql_err()?;
|
||||||
@@ -364,26 +379,17 @@ impl ProxyNodeReadRepository for MysqlProxyNodeReadRepository {
|
|||||||
node_id: &str,
|
node_id: &str,
|
||||||
limit: usize,
|
limit: usize,
|
||||||
) -> Result<Vec<StoredProxyNodeEvent>, DataLayerError> {
|
) -> Result<Vec<StoredProxyNodeEvent>, DataLayerError> {
|
||||||
let rows = sqlx::query(
|
let mut builder = QueryBuilder::<MySql>::new(PROXY_NODE_EVENT_COLUMNS);
|
||||||
r#"
|
let mut where_clause = WhereClause::new();
|
||||||
SELECT
|
push_eq(
|
||||||
id,
|
&mut builder,
|
||||||
node_id,
|
&mut where_clause,
|
||||||
event_type,
|
"node_id",
|
||||||
detail,
|
node_id.to_string(),
|
||||||
event_metadata,
|
);
|
||||||
created_at AS created_at_unix_ms
|
builder.push(" ORDER BY created_at DESC, id DESC");
|
||||||
FROM proxy_node_events
|
push_limit(&mut builder, i64::try_from(limit).unwrap_or(i64::MAX));
|
||||||
WHERE node_id = ?
|
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
||||||
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()
|
rows.iter().map(map_proxy_node_event_row).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -392,51 +398,36 @@ LIMIT ?
|
|||||||
node_id: &str,
|
node_id: &str,
|
||||||
query: &ProxyNodeEventQuery,
|
query: &ProxyNodeEventQuery,
|
||||||
) -> Result<Vec<StoredProxyNodeEvent>, DataLayerError> {
|
) -> Result<Vec<StoredProxyNodeEvent>, DataLayerError> {
|
||||||
let rows = sqlx::query(
|
let mut builder = QueryBuilder::<MySql>::new(PROXY_NODE_EVENT_COLUMNS);
|
||||||
r#"
|
let mut where_clause = WhereClause::new();
|
||||||
SELECT
|
push_eq(
|
||||||
id,
|
&mut builder,
|
||||||
node_id,
|
&mut where_clause,
|
||||||
event_type,
|
"node_id",
|
||||||
detail,
|
node_id.to_string(),
|
||||||
event_metadata,
|
);
|
||||||
created_at AS created_at_unix_ms
|
if let Some(from_unix_secs) = query.from_unix_secs {
|
||||||
FROM proxy_node_events
|
where_clause.push_next(&mut builder);
|
||||||
WHERE node_id = ?
|
builder
|
||||||
AND (? IS NULL OR created_at >= ?)
|
.push("created_at >= ")
|
||||||
AND (? IS NULL OR created_at <= ?)
|
.push_bind(i64::try_from(from_unix_secs).unwrap_or(i64::MAX));
|
||||||
AND (? IS NULL OR LOWER(event_type) = LOWER(?))
|
}
|
||||||
ORDER BY created_at DESC, id DESC
|
if let Some(to_unix_secs) = query.to_unix_secs {
|
||||||
LIMIT ?
|
where_clause.push_next(&mut builder);
|
||||||
"#,
|
builder
|
||||||
)
|
.push("created_at <= ")
|
||||||
.bind(node_id)
|
.push_bind(i64::try_from(to_unix_secs).unwrap_or(i64::MAX));
|
||||||
.bind(
|
}
|
||||||
query
|
if let Some(event_type) = query.event_type.as_deref() {
|
||||||
.from_unix_secs
|
where_clause.push_next(&mut builder);
|
||||||
.map(|v| i64::try_from(v).unwrap_or(i64::MAX)),
|
builder
|
||||||
)
|
.push("LOWER(event_type) = LOWER(")
|
||||||
.bind(
|
.push_bind(event_type.to_string())
|
||||||
query
|
.push(")");
|
||||||
.from_unix_secs
|
}
|
||||||
.map(|v| i64::try_from(v).unwrap_or(i64::MAX)),
|
builder.push(" ORDER BY created_at DESC, id DESC");
|
||||||
)
|
push_limit(&mut builder, i64::try_from(query.limit).unwrap_or(i64::MAX));
|
||||||
.bind(
|
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
||||||
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()
|
rows.iter().map(map_proxy_node_event_row).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use futures_util::TryStreamExt;
|
use futures_util::TryStreamExt;
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use sqlx::{postgres::PgRow, PgPool, Row};
|
use sqlx::{postgres::PgRow, PgPool, Postgres, QueryBuilder, Row};
|
||||||
|
|
||||||
use super::types::{
|
use super::types::{
|
||||||
bucket_start_unix_secs, build_tunnel_error_event_detail, build_tunnel_metrics_sample,
|
bucket_start_unix_secs, build_tunnel_error_event_detail, build_tunnel_metrics_sample,
|
||||||
@@ -17,6 +17,7 @@ use crate::{
|
|||||||
error::{postgres_error, SqlxResultExt},
|
error::{postgres_error, SqlxResultExt},
|
||||||
DataLayerError,
|
DataLayerError,
|
||||||
};
|
};
|
||||||
|
use aether_data_query::{push_eq, push_limit, WhereClause};
|
||||||
|
|
||||||
const FIND_PROXY_NODE_SQL: &str = r#"
|
const FIND_PROXY_NODE_SQL: &str = r#"
|
||||||
SELECT
|
SELECT
|
||||||
@@ -54,41 +55,6 @@ WHERE id = $1
|
|||||||
LIMIT 1
|
LIMIT 1
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
const LIST_PROXY_NODES_SQL: &str = r#"
|
|
||||||
SELECT
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
ip,
|
|
||||||
port,
|
|
||||||
region,
|
|
||||||
is_manual,
|
|
||||||
proxy_url,
|
|
||||||
proxy_username,
|
|
||||||
proxy_password,
|
|
||||||
CAST(status AS TEXT) AS status,
|
|
||||||
registered_by,
|
|
||||||
EXTRACT(EPOCH FROM last_heartbeat_at)::bigint AS last_heartbeat_at_unix_secs,
|
|
||||||
heartbeat_interval,
|
|
||||||
active_connections,
|
|
||||||
total_requests,
|
|
||||||
CAST(avg_latency_ms AS DOUBLE PRECISION) AS avg_latency_ms,
|
|
||||||
failed_requests,
|
|
||||||
dns_failures,
|
|
||||||
stream_errors,
|
|
||||||
proxy_metadata,
|
|
||||||
hardware_info,
|
|
||||||
estimated_max_concurrency,
|
|
||||||
tunnel_mode,
|
|
||||||
tunnel_connected,
|
|
||||||
EXTRACT(EPOCH FROM tunnel_connected_at)::bigint AS tunnel_connected_at_unix_secs,
|
|
||||||
remote_config,
|
|
||||||
config_version,
|
|
||||||
EXTRACT(EPOCH FROM created_at)::bigint AS created_at_unix_ms,
|
|
||||||
EXTRACT(EPOCH FROM updated_at)::bigint AS updated_at_unix_secs
|
|
||||||
FROM proxy_nodes
|
|
||||||
ORDER BY name ASC, id ASC
|
|
||||||
"#;
|
|
||||||
|
|
||||||
const LIST_PROXY_NODE_EVENTS_SQL: &str = r#"
|
const LIST_PROXY_NODE_EVENTS_SQL: &str = r#"
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
@@ -103,23 +69,6 @@ ORDER BY created_at DESC, id DESC
|
|||||||
LIMIT $2
|
LIMIT $2
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
const LIST_PROXY_NODE_EVENTS_FILTERED_SQL: &str = r#"
|
|
||||||
SELECT
|
|
||||||
id,
|
|
||||||
node_id,
|
|
||||||
CAST(event_type AS TEXT) AS event_type,
|
|
||||||
detail,
|
|
||||||
event_metadata,
|
|
||||||
EXTRACT(EPOCH FROM created_at)::bigint AS created_at_unix_ms
|
|
||||||
FROM proxy_node_events
|
|
||||||
WHERE node_id = $1
|
|
||||||
AND ($2::double precision IS NULL OR created_at >= TO_TIMESTAMP($2::double precision))
|
|
||||||
AND ($3::double precision IS NULL OR created_at <= TO_TIMESTAMP($3::double precision))
|
|
||||||
AND ($4::text IS NULL OR LOWER(CAST(event_type AS TEXT)) = LOWER($4::text))
|
|
||||||
ORDER BY created_at DESC, id DESC
|
|
||||||
LIMIT $5
|
|
||||||
"#;
|
|
||||||
|
|
||||||
const APPLY_HEARTBEAT_SQL: &str = r#"
|
const APPLY_HEARTBEAT_SQL: &str = r#"
|
||||||
UPDATE proxy_nodes
|
UPDATE proxy_nodes
|
||||||
SET
|
SET
|
||||||
@@ -870,7 +819,9 @@ impl SqlxProxyNodeRepository {
|
|||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl ProxyNodeReadRepository for SqlxProxyNodeRepository {
|
impl ProxyNodeReadRepository for SqlxProxyNodeRepository {
|
||||||
async fn list_proxy_nodes(&self) -> Result<Vec<StoredProxyNode>, DataLayerError> {
|
async fn list_proxy_nodes(&self) -> Result<Vec<StoredProxyNode>, DataLayerError> {
|
||||||
let mut rows = sqlx::query(LIST_PROXY_NODES_SQL).fetch(&self.pool);
|
let mut builder = QueryBuilder::<Postgres>::new(proxy_node_columns());
|
||||||
|
builder.push(" ORDER BY name ASC, id ASC");
|
||||||
|
let mut rows = builder.build().fetch(&self.pool);
|
||||||
let mut items = Vec::new();
|
let mut items = Vec::new();
|
||||||
while let Some(row) = rows.try_next().await.map_postgres_err()? {
|
while let Some(row) = rows.try_next().await.map_postgres_err()? {
|
||||||
items.push(Self::row_to_stored(&row)?);
|
items.push(Self::row_to_stored(&row)?);
|
||||||
@@ -882,8 +833,12 @@ impl ProxyNodeReadRepository for SqlxProxyNodeRepository {
|
|||||||
&self,
|
&self,
|
||||||
node_id: &str,
|
node_id: &str,
|
||||||
) -> Result<Option<StoredProxyNode>, DataLayerError> {
|
) -> Result<Option<StoredProxyNode>, DataLayerError> {
|
||||||
let row = sqlx::query(FIND_PROXY_NODE_SQL)
|
let mut builder = QueryBuilder::<Postgres>::new(proxy_node_columns());
|
||||||
.bind(node_id)
|
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()
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_postgres_err()?;
|
.map_postgres_err()?;
|
||||||
@@ -895,10 +850,17 @@ impl ProxyNodeReadRepository for SqlxProxyNodeRepository {
|
|||||||
node_id: &str,
|
node_id: &str,
|
||||||
limit: usize,
|
limit: usize,
|
||||||
) -> Result<Vec<StoredProxyNodeEvent>, DataLayerError> {
|
) -> Result<Vec<StoredProxyNodeEvent>, DataLayerError> {
|
||||||
let mut rows = sqlx::query(LIST_PROXY_NODE_EVENTS_SQL)
|
let mut builder = QueryBuilder::<Postgres>::new(proxy_node_event_columns());
|
||||||
.bind(node_id)
|
let mut where_clause = WhereClause::new();
|
||||||
.bind(i64::try_from(limit).unwrap_or(i64::MAX))
|
push_eq(
|
||||||
.fetch(&self.pool);
|
&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 mut rows = builder.build().fetch(&self.pool);
|
||||||
let mut items = Vec::new();
|
let mut items = Vec::new();
|
||||||
while let Some(row) = rows.try_next().await.map_postgres_err()? {
|
while let Some(row) = rows.try_next().await.map_postgres_err()? {
|
||||||
items.push(Self::row_to_event(&row)?);
|
items.push(Self::row_to_event(&row)?);
|
||||||
@@ -911,13 +873,38 @@ impl ProxyNodeReadRepository for SqlxProxyNodeRepository {
|
|||||||
node_id: &str,
|
node_id: &str,
|
||||||
query: &ProxyNodeEventQuery,
|
query: &ProxyNodeEventQuery,
|
||||||
) -> Result<Vec<StoredProxyNodeEvent>, DataLayerError> {
|
) -> Result<Vec<StoredProxyNodeEvent>, DataLayerError> {
|
||||||
let mut rows = sqlx::query(LIST_PROXY_NODE_EVENTS_FILTERED_SQL)
|
let mut builder = QueryBuilder::<Postgres>::new(proxy_node_event_columns());
|
||||||
.bind(node_id)
|
let mut where_clause = WhereClause::new();
|
||||||
.bind(query.from_unix_secs.map(|value| value as f64))
|
push_eq(
|
||||||
.bind(query.to_unix_secs.map(|value| value as f64))
|
&mut builder,
|
||||||
.bind(query.event_type.as_deref())
|
&mut where_clause,
|
||||||
.bind(i64::try_from(query.limit).unwrap_or(i64::MAX))
|
"node_id",
|
||||||
.fetch(&self.pool);
|
node_id.to_string(),
|
||||||
|
);
|
||||||
|
if let Some(from_unix_secs) = query.from_unix_secs {
|
||||||
|
where_clause.push_next(&mut builder);
|
||||||
|
builder
|
||||||
|
.push("created_at >= TO_TIMESTAMP(")
|
||||||
|
.push_bind(from_unix_secs as f64)
|
||||||
|
.push("::double precision)");
|
||||||
|
}
|
||||||
|
if let Some(to_unix_secs) = query.to_unix_secs {
|
||||||
|
where_clause.push_next(&mut builder);
|
||||||
|
builder
|
||||||
|
.push("created_at <= TO_TIMESTAMP(")
|
||||||
|
.push_bind(to_unix_secs as f64)
|
||||||
|
.push("::double precision)");
|
||||||
|
}
|
||||||
|
if let Some(event_type) = query.event_type.as_deref() {
|
||||||
|
where_clause.push_next(&mut builder);
|
||||||
|
builder
|
||||||
|
.push("LOWER(CAST(event_type AS TEXT)) = LOWER(")
|
||||||
|
.push_bind(event_type.to_string())
|
||||||
|
.push("::text)");
|
||||||
|
}
|
||||||
|
builder.push(" ORDER BY created_at DESC, id DESC");
|
||||||
|
push_limit(&mut builder, i64::try_from(query.limit).unwrap_or(i64::MAX));
|
||||||
|
let mut rows = builder.build().fetch(&self.pool);
|
||||||
let mut items = Vec::new();
|
let mut items = Vec::new();
|
||||||
while let Some(row) = rows.try_next().await.map_postgres_err()? {
|
while let Some(row) = rows.try_next().await.map_postgres_err()? {
|
||||||
items.push(Self::row_to_event(&row)?);
|
items.push(Self::row_to_event(&row)?);
|
||||||
@@ -974,6 +961,20 @@ impl ProxyNodeReadRepository for SqlxProxyNodeRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn proxy_node_columns() -> &'static str {
|
||||||
|
FIND_PROXY_NODE_SQL
|
||||||
|
.split_once("WHERE id = $1")
|
||||||
|
.map(|(prefix, _)| prefix)
|
||||||
|
.unwrap_or(FIND_PROXY_NODE_SQL)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn proxy_node_event_columns() -> &'static str {
|
||||||
|
LIST_PROXY_NODE_EVENTS_SQL
|
||||||
|
.split_once("WHERE node_id = $1")
|
||||||
|
.map(|(prefix, _)| prefix)
|
||||||
|
.unwrap_or(LIST_PROXY_NODE_EVENTS_SQL)
|
||||||
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl ProxyNodeWriteRepository for SqlxProxyNodeRepository {
|
impl ProxyNodeWriteRepository for SqlxProxyNodeRepository {
|
||||||
async fn reset_stale_tunnel_statuses(&self) -> Result<usize, DataLayerError> {
|
async fn reset_stale_tunnel_statuses(&self) -> Result<usize, DataLayerError> {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use sqlx::{sqlite::SqliteRow, Row};
|
use sqlx::{sqlite::SqliteRow, QueryBuilder, Row, Sqlite};
|
||||||
|
|
||||||
use super::types::{
|
use super::types::{
|
||||||
bucket_start_unix_secs, build_tunnel_error_event_detail, build_tunnel_metrics_sample,
|
bucket_start_unix_secs, build_tunnel_error_event_detail, build_tunnel_metrics_sample,
|
||||||
@@ -14,6 +14,7 @@ use super::types::{
|
|||||||
use crate::driver::sqlite::SqlitePool;
|
use crate::driver::sqlite::SqlitePool;
|
||||||
use crate::error::SqlResultExt;
|
use crate::error::SqlResultExt;
|
||||||
use crate::DataLayerError;
|
use crate::DataLayerError;
|
||||||
|
use aether_data_query::{push_eq, push_limit, WhereClause};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct SqliteProxyNodeReadRepository {
|
pub struct SqliteProxyNodeReadRepository {
|
||||||
@@ -337,13 +338,23 @@ SELECT
|
|||||||
FROM proxy_nodes
|
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]
|
#[async_trait]
|
||||||
impl ProxyNodeReadRepository for SqliteProxyNodeReadRepository {
|
impl ProxyNodeReadRepository for SqliteProxyNodeReadRepository {
|
||||||
async fn list_proxy_nodes(&self) -> Result<Vec<StoredProxyNode>, DataLayerError> {
|
async fn list_proxy_nodes(&self) -> Result<Vec<StoredProxyNode>, DataLayerError> {
|
||||||
let rows = sqlx::query(&format!("{PROXY_NODE_COLUMNS} ORDER BY name ASC, id ASC"))
|
let mut builder = QueryBuilder::<Sqlite>::new(PROXY_NODE_COLUMNS);
|
||||||
.fetch_all(&self.pool)
|
builder.push(" ORDER BY name ASC, id ASC");
|
||||||
.await
|
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
||||||
.map_sql_err()?;
|
|
||||||
rows.iter().map(map_proxy_node_row).collect()
|
rows.iter().map(map_proxy_node_row).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,8 +362,12 @@ impl ProxyNodeReadRepository for SqliteProxyNodeReadRepository {
|
|||||||
&self,
|
&self,
|
||||||
node_id: &str,
|
node_id: &str,
|
||||||
) -> Result<Option<StoredProxyNode>, DataLayerError> {
|
) -> Result<Option<StoredProxyNode>, DataLayerError> {
|
||||||
let row = sqlx::query(&format!("{PROXY_NODE_COLUMNS} WHERE id = ? LIMIT 1"))
|
let mut builder = QueryBuilder::<Sqlite>::new(PROXY_NODE_COLUMNS);
|
||||||
.bind(node_id)
|
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()
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_sql_err()?;
|
.map_sql_err()?;
|
||||||
@@ -364,26 +379,17 @@ impl ProxyNodeReadRepository for SqliteProxyNodeReadRepository {
|
|||||||
node_id: &str,
|
node_id: &str,
|
||||||
limit: usize,
|
limit: usize,
|
||||||
) -> Result<Vec<StoredProxyNodeEvent>, DataLayerError> {
|
) -> Result<Vec<StoredProxyNodeEvent>, DataLayerError> {
|
||||||
let rows = sqlx::query(
|
let mut builder = QueryBuilder::<Sqlite>::new(PROXY_NODE_EVENT_COLUMNS);
|
||||||
r#"
|
let mut where_clause = WhereClause::new();
|
||||||
SELECT
|
push_eq(
|
||||||
id,
|
&mut builder,
|
||||||
node_id,
|
&mut where_clause,
|
||||||
event_type,
|
"node_id",
|
||||||
detail,
|
node_id.to_string(),
|
||||||
event_metadata,
|
);
|
||||||
created_at AS created_at_unix_ms
|
builder.push(" ORDER BY created_at DESC, id DESC");
|
||||||
FROM proxy_node_events
|
push_limit(&mut builder, i64::try_from(limit).unwrap_or(i64::MAX));
|
||||||
WHERE node_id = ?
|
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
||||||
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()
|
rows.iter().map(map_proxy_node_event_row).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -392,51 +398,36 @@ LIMIT ?
|
|||||||
node_id: &str,
|
node_id: &str,
|
||||||
query: &ProxyNodeEventQuery,
|
query: &ProxyNodeEventQuery,
|
||||||
) -> Result<Vec<StoredProxyNodeEvent>, DataLayerError> {
|
) -> Result<Vec<StoredProxyNodeEvent>, DataLayerError> {
|
||||||
let rows = sqlx::query(
|
let mut builder = QueryBuilder::<Sqlite>::new(PROXY_NODE_EVENT_COLUMNS);
|
||||||
r#"
|
let mut where_clause = WhereClause::new();
|
||||||
SELECT
|
push_eq(
|
||||||
id,
|
&mut builder,
|
||||||
node_id,
|
&mut where_clause,
|
||||||
event_type,
|
"node_id",
|
||||||
detail,
|
node_id.to_string(),
|
||||||
event_metadata,
|
);
|
||||||
created_at AS created_at_unix_ms
|
if let Some(from_unix_secs) = query.from_unix_secs {
|
||||||
FROM proxy_node_events
|
where_clause.push_next(&mut builder);
|
||||||
WHERE node_id = ?
|
builder
|
||||||
AND (? IS NULL OR created_at >= ?)
|
.push("created_at >= ")
|
||||||
AND (? IS NULL OR created_at <= ?)
|
.push_bind(i64::try_from(from_unix_secs).unwrap_or(i64::MAX));
|
||||||
AND (? IS NULL OR LOWER(event_type) = LOWER(?))
|
}
|
||||||
ORDER BY created_at DESC, id DESC
|
if let Some(to_unix_secs) = query.to_unix_secs {
|
||||||
LIMIT ?
|
where_clause.push_next(&mut builder);
|
||||||
"#,
|
builder
|
||||||
)
|
.push("created_at <= ")
|
||||||
.bind(node_id)
|
.push_bind(i64::try_from(to_unix_secs).unwrap_or(i64::MAX));
|
||||||
.bind(
|
}
|
||||||
query
|
if let Some(event_type) = query.event_type.as_deref() {
|
||||||
.from_unix_secs
|
where_clause.push_next(&mut builder);
|
||||||
.map(|v| i64::try_from(v).unwrap_or(i64::MAX)),
|
builder
|
||||||
)
|
.push("LOWER(event_type) = LOWER(")
|
||||||
.bind(
|
.push_bind(event_type.to_string())
|
||||||
query
|
.push(")");
|
||||||
.from_unix_secs
|
}
|
||||||
.map(|v| i64::try_from(v).unwrap_or(i64::MAX)),
|
builder.push(" ORDER BY created_at DESC, id DESC");
|
||||||
)
|
push_limit(&mut builder, i64::try_from(query.limit).unwrap_or(i64::MAX));
|
||||||
.bind(
|
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
||||||
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()
|
rows.iter().map(map_proxy_node_event_row).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use super::{
|
|||||||
use crate::driver::mysql::MysqlPool;
|
use crate::driver::mysql::MysqlPool;
|
||||||
use crate::error::SqlResultExt;
|
use crate::error::SqlResultExt;
|
||||||
use crate::DataLayerError;
|
use crate::DataLayerError;
|
||||||
|
use aether_data_query::{push_in, WhereClause};
|
||||||
|
|
||||||
const QUOTA_COLUMNS: &str = r#"
|
const QUOTA_COLUMNS: &str = r#"
|
||||||
SELECT
|
SELECT
|
||||||
@@ -55,14 +56,9 @@ impl ProviderQuotaReadRepository for MysqlProviderQuotaRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut builder = QueryBuilder::<MySql>::new(QUOTA_COLUMNS);
|
let mut builder = QueryBuilder::<MySql>::new(QUOTA_COLUMNS);
|
||||||
builder.push(" WHERE id IN (");
|
let mut where_clause = WhereClause::new();
|
||||||
{
|
push_in(&mut builder, &mut where_clause, "id", provider_ids);
|
||||||
let mut separated = builder.separated(", ");
|
builder.push(" ORDER BY id ASC");
|
||||||
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()?;
|
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
||||||
rows.iter().map(map_row).collect()
|
rows.iter().map(map_row).collect()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use sqlx::{PgPool, Row};
|
use sqlx::{PgPool, Postgres, QueryBuilder, Row};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
ProviderQuotaReadRepository, ProviderQuotaWriteRepository, StoredProviderQuotaSnapshot,
|
ProviderQuotaReadRepository, ProviderQuotaWriteRepository, StoredProviderQuotaSnapshot,
|
||||||
};
|
};
|
||||||
use crate::{error::SqlxResultExt, DataLayerError};
|
use crate::{error::SqlxResultExt, DataLayerError};
|
||||||
|
use aether_data_query::{push_in, WhereClause};
|
||||||
|
|
||||||
const FIND_BY_PROVIDER_ID_SQL: &str = r#"
|
const FIND_BY_PROVIDER_ID_SQL: &str = r#"
|
||||||
SELECT
|
SELECT
|
||||||
@@ -21,21 +22,6 @@ WHERE id = $1
|
|||||||
LIMIT 1
|
LIMIT 1
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
const FIND_BY_PROVIDER_IDS_SQL: &str = r#"
|
|
||||||
SELECT
|
|
||||||
id AS provider_id,
|
|
||||||
CAST(billing_type AS TEXT) AS billing_type,
|
|
||||||
CAST(monthly_quota_usd AS DOUBLE PRECISION) AS monthly_quota_usd,
|
|
||||||
CAST(COALESCE(monthly_used_usd, 0) AS DOUBLE PRECISION) AS monthly_used_usd,
|
|
||||||
quota_reset_day,
|
|
||||||
CAST(EXTRACT(EPOCH FROM quota_last_reset_at) AS BIGINT) AS quota_last_reset_at_unix_secs,
|
|
||||||
CAST(EXTRACT(EPOCH FROM quota_expires_at) AS BIGINT) AS quota_expires_at_unix_secs,
|
|
||||||
is_active
|
|
||||||
FROM providers
|
|
||||||
WHERE id = ANY($1::TEXT[])
|
|
||||||
ORDER BY id ASC
|
|
||||||
"#;
|
|
||||||
|
|
||||||
const RESET_DUE_SQL: &str = r#"
|
const RESET_DUE_SQL: &str = r#"
|
||||||
UPDATE providers
|
UPDATE providers
|
||||||
SET
|
SET
|
||||||
@@ -83,9 +69,14 @@ impl ProviderQuotaReadRepository for SqlxProviderQuotaRepository {
|
|||||||
if provider_ids.is_empty() {
|
if provider_ids.is_empty() {
|
||||||
return Ok(Vec::new());
|
return Ok(Vec::new());
|
||||||
}
|
}
|
||||||
|
let mut builder = QueryBuilder::<Postgres>::new(
|
||||||
sqlx::query(FIND_BY_PROVIDER_IDS_SQL)
|
"SELECT id AS provider_id, CAST(billing_type AS TEXT) AS billing_type, CAST(monthly_quota_usd AS DOUBLE PRECISION) AS monthly_quota_usd, CAST(COALESCE(monthly_used_usd, 0) AS DOUBLE PRECISION) AS monthly_used_usd, quota_reset_day, CAST(EXTRACT(EPOCH FROM quota_last_reset_at) AS BIGINT) AS quota_last_reset_at_unix_secs, CAST(EXTRACT(EPOCH FROM quota_expires_at) AS BIGINT) AS quota_expires_at_unix_secs, is_active FROM providers",
|
||||||
.bind(provider_ids)
|
);
|
||||||
|
let mut where_clause = WhereClause::new();
|
||||||
|
push_in(&mut builder, &mut where_clause, "id", provider_ids);
|
||||||
|
builder.push(" ORDER BY id ASC");
|
||||||
|
builder
|
||||||
|
.build()
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_postgres_err()?
|
.map_postgres_err()?
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use super::{
|
|||||||
use crate::driver::sqlite::{sqlite_optional_real, sqlite_real, SqlitePool};
|
use crate::driver::sqlite::{sqlite_optional_real, sqlite_real, SqlitePool};
|
||||||
use crate::error::SqlResultExt;
|
use crate::error::SqlResultExt;
|
||||||
use crate::DataLayerError;
|
use crate::DataLayerError;
|
||||||
|
use aether_data_query::{push_in, WhereClause};
|
||||||
|
|
||||||
const QUOTA_COLUMNS: &str = r#"
|
const QUOTA_COLUMNS: &str = r#"
|
||||||
SELECT
|
SELECT
|
||||||
@@ -55,14 +56,9 @@ impl ProviderQuotaReadRepository for SqliteProviderQuotaRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut builder = QueryBuilder::<Sqlite>::new(QUOTA_COLUMNS);
|
let mut builder = QueryBuilder::<Sqlite>::new(QUOTA_COLUMNS);
|
||||||
builder.push(" WHERE id IN (");
|
let mut where_clause = WhereClause::new();
|
||||||
{
|
push_in(&mut builder, &mut where_clause, "id", provider_ids);
|
||||||
let mut separated = builder.separated(", ");
|
builder.push(" ORDER BY id ASC");
|
||||||
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()?;
|
let rows = builder.build().fetch_all(&self.pool).await.map_sql_err()?;
|
||||||
rows.iter().map(map_row).collect()
|
rows.iter().map(map_row).collect()
|
||||||
}
|
}
|
||||||
|
|||||||
78
docs/development/simple-query-inventory.md
Normal file
78
docs/development/simple-query-inventory.md
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
# Aether Data Simple Query Inventory
|
||||||
|
|
||||||
|
This inventory tracks repository read paths that are intended to use the
|
||||||
|
internal `aether-data-query` helpers. The helper layer is for SQL fragments only:
|
||||||
|
repositories still own their table-specific projections, joins, row mapping, and
|
||||||
|
business rules.
|
||||||
|
|
||||||
|
## Included In This Pass
|
||||||
|
|
||||||
|
- `background_tasks`
|
||||||
|
- `find_run`
|
||||||
|
- `list_runs`
|
||||||
|
- `list_events`
|
||||||
|
- simple `summarize_runs` count/group reads remain behavior-locked in SQL
|
||||||
|
- `announcements`
|
||||||
|
- `find_by_id`
|
||||||
|
- `list_announcements`
|
||||||
|
- `count_unread_active_announcements`
|
||||||
|
- `auth_modules`
|
||||||
|
- `list_enabled_oauth_providers`
|
||||||
|
- `get_ldap_config`
|
||||||
|
- `oauth_providers`
|
||||||
|
- `list_oauth_provider_configs`
|
||||||
|
- `get_oauth_provider_config`
|
||||||
|
- `quota`
|
||||||
|
- `find_by_provider_id`
|
||||||
|
- `find_by_provider_ids`
|
||||||
|
- `provider_catalog`
|
||||||
|
- provider by-id/provider list reads in PG/SQLite
|
||||||
|
- endpoint/key by-id and by-provider-id `IN` reads in PG/SQLite
|
||||||
|
- provider key page filters, search, order, limit/offset in PG/SQLite
|
||||||
|
- key stats by provider ids in PG/SQLite
|
||||||
|
- `proxy_nodes`
|
||||||
|
- node list/find reads
|
||||||
|
- event list/filter reads
|
||||||
|
- `management_tokens`
|
||||||
|
- `list_management_tokens`
|
||||||
|
- `get_management_token_with_user`
|
||||||
|
- `get_management_token_with_user_by_hash`
|
||||||
|
- `pool_scores`
|
||||||
|
- `find_scores_by_identity`
|
||||||
|
- `list_ranked_pool_members`
|
||||||
|
- `list_pool_member_scores`
|
||||||
|
- `list_pool_member_probe_candidates`
|
||||||
|
- `get_pool_member_scores_by_ids`
|
||||||
|
- `candidates`
|
||||||
|
- `list_by_request_id`
|
||||||
|
- `list_recent`
|
||||||
|
- `list_by_provider_id`
|
||||||
|
- `list_finalized_by_endpoint_ids_since`
|
||||||
|
- simple finalized status count
|
||||||
|
- `gemini_file_mappings`
|
||||||
|
- list/count filters and search
|
||||||
|
|
||||||
|
## Deferred
|
||||||
|
|
||||||
|
- `usage` aggregation, dashboard, leaderboard, cache-hit, provider/key/user
|
||||||
|
statistics, rebuild paths, and body/blob reads.
|
||||||
|
- `candidate_selection` JSON/alias matching and scoring.
|
||||||
|
- `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.
|
||||||
|
|
||||||
|
## Helper Coverage
|
||||||
|
|
||||||
|
- dialect-aware identifier quoting
|
||||||
|
- `WHERE`/`AND` sequencing
|
||||||
|
- equality and optional equality filters
|
||||||
|
- `IN` filters
|
||||||
|
- case-insensitive contains/search
|
||||||
|
- whitelisted order-by rendering
|
||||||
|
- `LIMIT` and `LIMIT/OFFSET`
|
||||||
Reference in New Issue
Block a user