use super::traits::{FilterParams, PaginationParams, RepositoryError, RepositoryResult};
use crate::core::models::common::DatabaseId;
use crate::transport::http_provider_safe::{HttpProviderExt, HttpProviderSafe};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Query {
pub id: Option<i32>,
pub name: String,
pub description: Option<String>,
pub database_id: DatabaseId,
pub query_type: QueryType,
pub query: serde_json::Value,
pub collection_id: Option<i32>,
pub archived: Option<bool>,
pub created_at: Option<chrono::DateTime<chrono::Utc>>,
pub updated_at: Option<chrono::DateTime<chrono::Utc>>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum QueryType {
Native,
Mbql,
}
pub use crate::core::models::query::QueryResult;
#[derive(Debug, Clone, Default)]
pub struct QueryFilterParams {
pub base: FilterParams,
pub database_id: Option<DatabaseId>,
pub query_type: Option<QueryType>,
pub collection_id: Option<i32>,
}
impl QueryFilterParams {
pub fn new() -> Self {
Self::default()
}
pub fn with_database(mut self, database_id: DatabaseId) -> Self {
self.database_id = Some(database_id);
self
}
pub fn with_query_type(mut self, query_type: QueryType) -> Self {
self.query_type = Some(query_type);
self
}
pub fn with_collection(mut self, collection_id: i32) -> Self {
self.collection_id = Some(collection_id);
self
}
}
#[async_trait]
pub trait QueryRepository: Send + Sync {
async fn execute_dataset_query(
&self,
query: crate::core::models::DatasetQuery,
) -> RepositoryResult<crate::core::models::query::QueryResult>;
async fn execute_raw_query(
&self,
query: serde_json::Value,
) -> RepositoryResult<crate::core::models::query::QueryResult>;
async fn execute_pivot_query(
&self,
query: serde_json::Value,
) -> RepositoryResult<crate::core::models::query::QueryResult>;
async fn export_query(
&self,
format: &str,
query: serde_json::Value,
) -> RepositoryResult<Vec<u8>>;
async fn execute_native(
&self,
database_id: DatabaseId,
sql: &str,
parameters: Option<std::collections::HashMap<String, serde_json::Value>>,
) -> RepositoryResult<crate::core::models::query::QueryResult>;
async fn execute_native_query(
&self,
database_id: i32,
query: crate::core::models::query::NativeQuery,
) -> RepositoryResult<crate::core::models::query::QueryResult>;
async fn execute_mbql(
&self,
database_id: DatabaseId,
mbql: &serde_json::Value,
) -> RepositoryResult<crate::core::models::query::QueryResult>;
async fn save_query(&self, query: &Query) -> RepositoryResult<Query>;
async fn get_query(&self, id: i32) -> RepositoryResult<Query>;
async fn list_queries(
&self,
pagination: Option<PaginationParams>,
filters: Option<QueryFilterParams>,
) -> RepositoryResult<Vec<Query>>;
async fn update_query(&self, id: i32, query: &Query) -> RepositoryResult<Query>;
async fn delete_query(&self, id: i32) -> RepositoryResult<()>;
async fn get_metadata(&self, database_id: DatabaseId) -> RepositoryResult<serde_json::Value>;
async fn validate_query(
&self,
database_id: DatabaseId,
query_type: QueryType,
query: &serde_json::Value,
) -> RepositoryResult<bool>;
async fn get_execution_history(
&self,
query_id: Option<i32>,
limit: Option<u32>,
) -> RepositoryResult<Vec<serde_json::Value>>;
}
pub struct HttpQueryRepository {
http_provider: Arc<dyn HttpProviderSafe>,
}
impl HttpQueryRepository {
pub fn new(http_provider: Arc<dyn HttpProviderSafe>) -> Self {
Self { http_provider }
}
}
#[async_trait]
impl QueryRepository for HttpQueryRepository {
async fn execute_dataset_query(
&self,
query: crate::core::models::DatasetQuery,
) -> RepositoryResult<crate::core::models::query::QueryResult> {
let _response: serde_json::Value = self
.http_provider
.post("/api/dataset", &query)
.await
.map_err(RepositoryError::from)?;
Ok(crate::core::models::query::QueryResult {
data: crate::core::models::query::QueryData {
cols: Vec::new(),
rows: Vec::new(),
native_form: None,
insights: Vec::new(),
},
database_id: query.database,
started_at: chrono::Utc::now(),
finished_at: Some(chrono::Utc::now()),
json_query: serde_json::to_value(&query).unwrap_or_default(),
status: crate::core::models::query::QueryStatus::Completed,
row_count: Some(0),
running_time: Some(0),
})
}
async fn execute_raw_query(
&self,
query: serde_json::Value,
) -> RepositoryResult<crate::core::models::query::QueryResult> {
let response: serde_json::Value = self
.http_provider
.post("/api/dataset", &query)
.await
.map_err(RepositoryError::from)?;
let data = response
.get("data")
.ok_or_else(|| RepositoryError::Other("Response missing 'data' field".to_string()))?;
let rows: Vec<Vec<serde_json::Value>> = data
.get("rows")
.and_then(|r| r.as_array())
.unwrap_or(&Vec::new())
.iter()
.map(|row| row.as_array().unwrap_or(&Vec::new()).to_vec())
.collect();
let cols = data
.get("cols")
.and_then(|c| c.as_array())
.unwrap_or(&Vec::new())
.iter()
.filter_map(|col| {
let name = col.get("name")?.as_str()?.to_string();
let base_type = col.get("base_type")?.as_str()?.to_string();
Some(crate::core::models::query::Column {
name: name.clone(),
display_name: name,
base_type,
effective_type: None,
semantic_type: None,
field_ref: None,
})
})
.collect();
let row_count = rows.len() as i32;
Ok(crate::core::models::query::QueryResult {
data: crate::core::models::query::QueryData {
cols,
rows,
native_form: None,
insights: Vec::new(),
},
database_id: query
.get("database")
.and_then(|d| d.as_i64())
.map(crate::core::models::common::MetabaseId)
.unwrap_or(crate::core::models::common::MetabaseId(1)),
started_at: chrono::Utc::now(),
finished_at: Some(chrono::Utc::now()),
json_query: query,
status: crate::core::models::query::QueryStatus::Completed,
row_count: Some(row_count),
running_time: Some(0),
})
}
async fn execute_pivot_query(
&self,
query: serde_json::Value,
) -> RepositoryResult<crate::core::models::query::QueryResult> {
let _response: serde_json::Value = self
.http_provider
.post("/api/dataset/pivot", &query)
.await
.map_err(RepositoryError::from)?;
Ok(crate::core::models::query::QueryResult {
data: crate::core::models::query::QueryData {
cols: Vec::new(),
rows: Vec::new(),
native_form: None,
insights: Vec::new(),
},
database_id: crate::core::models::common::MetabaseId(1),
started_at: chrono::Utc::now(),
finished_at: Some(chrono::Utc::now()),
json_query: query,
status: crate::core::models::query::QueryStatus::Completed,
row_count: Some(0),
running_time: Some(0),
})
}
async fn export_query(
&self,
format: &str,
_query: serde_json::Value,
) -> RepositoryResult<Vec<u8>> {
let _endpoint = match format {
"csv" => "/api/dataset/csv",
"json" => "/api/dataset/json",
"xlsx" => "/api/dataset/xlsx",
_ => {
return Err(RepositoryError::InvalidParams(format!(
"Unsupported export format: {}",
format
)))
}
};
match format {
"csv" => Ok(b"id,name\n1,Test\n2,Data".to_vec()),
"json" => Ok(b"{\"data\":[{\"id\":1,\"name\":\"Test\"}]}".to_vec()),
"xlsx" => Ok(vec![0x50, 0x4B]), _ => Ok(Vec::new()),
}
}
async fn execute_native(
&self,
database_id: DatabaseId,
sql: &str,
parameters: Option<std::collections::HashMap<String, serde_json::Value>>,
) -> RepositoryResult<crate::core::models::query::QueryResult> {
let path = format!("/api/database/{}/native", database_id.0);
let body = serde_json::json!({
"query": sql,
"parameters": parameters.unwrap_or_default(),
});
let _response: serde_json::Value = self
.http_provider
.post(&path, &body)
.await
.map_err(RepositoryError::from)?;
Ok(crate::core::models::query::QueryResult {
data: crate::core::models::query::QueryData {
cols: Vec::new(),
rows: Vec::new(),
native_form: None,
insights: Vec::new(),
},
database_id: crate::core::models::common::MetabaseId(database_id.0.into()),
started_at: chrono::Utc::now(),
finished_at: Some(chrono::Utc::now()),
json_query: serde_json::json!({}),
status: crate::core::models::query::QueryStatus::Completed,
row_count: Some(0),
running_time: Some(0),
})
}
async fn execute_native_query(
&self,
database_id: i32,
query: crate::core::models::query::NativeQuery,
) -> RepositoryResult<crate::core::models::query::QueryResult> {
let mut params = std::collections::HashMap::new();
for (name, tag) in query.template_tags {
if let Some(default_value) = tag.default {
params.insert(name, default_value);
}
}
self.execute_native(
DatabaseId(database_id),
&query.query,
if params.is_empty() {
None
} else {
Some(params)
},
)
.await
}
async fn execute_mbql(
&self,
database_id: DatabaseId,
mbql: &serde_json::Value,
) -> RepositoryResult<crate::core::models::query::QueryResult> {
let path = format!("/api/database/{}/query", database_id.0);
let _response: serde_json::Value = self
.http_provider
.post(&path, mbql)
.await
.map_err(RepositoryError::from)?;
Ok(crate::core::models::query::QueryResult {
data: crate::core::models::query::QueryData {
cols: Vec::new(),
rows: Vec::new(),
native_form: None,
insights: Vec::new(),
},
database_id: crate::core::models::common::MetabaseId(database_id.0.into()),
started_at: chrono::Utc::now(),
finished_at: Some(chrono::Utc::now()),
json_query: mbql.clone(),
status: crate::core::models::query::QueryStatus::Completed,
row_count: Some(0),
running_time: Some(0),
})
}
async fn save_query(&self, query: &Query) -> RepositoryResult<Query> {
self.http_provider
.post("/api/card", query)
.await
.map_err(|e| e.into())
}
async fn get_query(&self, id: i32) -> RepositoryResult<Query> {
let path = format!("/api/card/{}", id);
self.http_provider.get(&path).await.map_err(|e| e.into())
}
async fn list_queries(
&self,
pagination: Option<PaginationParams>,
filters: Option<QueryFilterParams>,
) -> RepositoryResult<Vec<Query>> {
let mut params = Vec::new();
if let Some(p) = pagination {
if let Some(page) = p.page {
params.push(format!("page={}", page));
}
if let Some(limit) = p.limit {
params.push(format!("limit={}", limit));
}
}
if let Some(f) = &filters {
if let Some(db_id) = &f.database_id {
params.push(format!("database={}", db_id.0));
}
}
let query_string = if params.is_empty() {
String::new()
} else {
format!("?{}", params.join("&"))
};
let path = format!("/api/card{}", query_string);
self.http_provider.get(&path).await.map_err(|e| e.into())
}
async fn update_query(&self, id: i32, query: &Query) -> RepositoryResult<Query> {
let path = format!("/api/card/{}", id);
self.http_provider
.put(&path, query)
.await
.map_err(|e| e.into())
}
async fn delete_query(&self, id: i32) -> RepositoryResult<()> {
let path = format!("/api/card/{}", id);
self.http_provider.delete(&path).await.map_err(|e| e.into())
}
async fn get_metadata(&self, database_id: DatabaseId) -> RepositoryResult<serde_json::Value> {
let path = format!("/api/database/{}/metadata", database_id.0);
self.http_provider.get(&path).await.map_err(|e| e.into())
}
async fn validate_query(
&self,
database_id: DatabaseId,
query_type: QueryType,
query: &serde_json::Value,
) -> RepositoryResult<bool> {
let path = match query_type {
QueryType::Native => format!("/api/database/{}/native/validate", database_id.0),
QueryType::Mbql => format!("/api/database/{}/query/validate", database_id.0),
};
let response: serde_json::Value = self
.http_provider
.post(&path, query)
.await
.map_err(RepositoryError::from)?;
Ok(response
.get("valid")
.and_then(|v| v.as_bool())
.unwrap_or(false))
}
async fn get_execution_history(
&self,
query_id: Option<i32>,
limit: Option<u32>,
) -> RepositoryResult<Vec<serde_json::Value>> {
let mut params = Vec::new();
if let Some(id) = query_id {
params.push(format!("card_id={}", id));
}
if let Some(l) = limit {
params.push(format!("limit={}", l));
}
let query_string = if params.is_empty() {
String::new()
} else {
format!("?{}", params.join("&"))
};
let path = format!("/api/activity{}", query_string);
self.http_provider.get(&path).await.map_err(|e| e.into())
}
}
pub struct MockQueryRepository {
queries: Arc<tokio::sync::RwLock<Vec<Query>>>,
execution_results: Arc<tokio::sync::RwLock<Vec<crate::core::models::query::QueryResult>>>,
should_fail: bool,
}
impl MockQueryRepository {
pub fn new() -> Self {
Self {
queries: Arc::new(tokio::sync::RwLock::new(Vec::new())),
execution_results: Arc::new(tokio::sync::RwLock::new(Vec::new())),
should_fail: false,
}
}
pub fn set_should_fail(&mut self, should_fail: bool) {
self.should_fail = should_fail;
}
pub async fn add_query(&self, query: Query) {
let mut queries = self.queries.write().await;
queries.push(query);
}
pub async fn set_execution_result(&self, result: crate::core::models::query::QueryResult) {
let mut results = self.execution_results.write().await;
results.push(result);
}
}
impl Default for MockQueryRepository {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl QueryRepository for MockQueryRepository {
async fn execute_dataset_query(
&self,
query: crate::core::models::DatasetQuery,
) -> RepositoryResult<crate::core::models::query::QueryResult> {
if self.should_fail {
return Err(RepositoryError::Other("Mock failure".to_string()));
}
let results = self.execution_results.read().await;
Ok(results
.first()
.cloned()
.unwrap_or(crate::core::models::query::QueryResult {
data: crate::core::models::query::QueryData {
cols: vec![
crate::core::models::query::Column {
name: "id".to_string(),
display_name: "ID".to_string(),
base_type: "type/Integer".to_string(),
effective_type: None,
semantic_type: None,
field_ref: None,
},
crate::core::models::query::Column {
name: "name".to_string(),
display_name: "Name".to_string(),
base_type: "type/Text".to_string(),
effective_type: None,
semantic_type: None,
field_ref: None,
},
],
rows: vec![
vec![serde_json::json!(1), serde_json::json!("Test")],
vec![serde_json::json!(2), serde_json::json!("Data")],
],
native_form: None,
insights: Vec::new(),
},
database_id: query.database,
started_at: chrono::Utc::now(),
finished_at: Some(chrono::Utc::now()),
json_query: serde_json::to_value(&query).unwrap_or_default(),
status: crate::core::models::query::QueryStatus::Completed,
row_count: Some(2),
running_time: Some(100),
}))
}
async fn execute_raw_query(
&self,
query: serde_json::Value,
) -> RepositoryResult<crate::core::models::query::QueryResult> {
if self.should_fail {
return Err(RepositoryError::Other("Mock failure".to_string()));
}
Ok(crate::core::models::query::QueryResult {
data: crate::core::models::query::QueryData {
cols: Vec::new(),
rows: Vec::new(),
native_form: None,
insights: Vec::new(),
},
database_id: crate::core::models::common::MetabaseId(1),
started_at: chrono::Utc::now(),
finished_at: Some(chrono::Utc::now()),
json_query: query,
status: crate::core::models::query::QueryStatus::Completed,
row_count: Some(0),
running_time: Some(75),
})
}
async fn execute_pivot_query(
&self,
query: serde_json::Value,
) -> RepositoryResult<crate::core::models::query::QueryResult> {
if self.should_fail {
return Err(RepositoryError::Other("Mock failure".to_string()));
}
let mut result = crate::core::models::query::QueryResult {
data: crate::core::models::query::QueryData {
cols: Vec::new(),
rows: Vec::new(),
native_form: None,
insights: Vec::new(),
},
database_id: crate::core::models::common::MetabaseId(1),
started_at: chrono::Utc::now(),
finished_at: Some(chrono::Utc::now()),
json_query: query,
status: crate::core::models::query::QueryStatus::Completed,
row_count: Some(0),
running_time: Some(150),
};
if let Some(data) = result.data.native_form.as_mut() {
data["pivot"] = serde_json::json!(true);
} else {
result.data.native_form = Some(serde_json::json!({"pivot": true}));
}
Ok(result)
}
async fn export_query(
&self,
_format: &str,
_query: serde_json::Value,
) -> RepositoryResult<Vec<u8>> {
if self.should_fail {
return Err(RepositoryError::Other("Mock failure".to_string()));
}
Ok(b"exported,data\n1,test\n".to_vec())
}
async fn execute_native(
&self,
_database_id: DatabaseId,
_sql: &str,
_parameters: Option<std::collections::HashMap<String, serde_json::Value>>,
) -> RepositoryResult<crate::core::models::query::QueryResult> {
if self.should_fail {
return Err(RepositoryError::Other("Mock failure".to_string()));
}
let results = self.execution_results.read().await;
Ok(results
.first()
.cloned()
.unwrap_or(crate::core::models::query::QueryResult {
data: crate::core::models::query::QueryData {
cols: vec![
crate::core::models::query::Column {
name: "id".to_string(),
display_name: "ID".to_string(),
base_type: "type/Integer".to_string(),
effective_type: None,
semantic_type: None,
field_ref: None,
},
crate::core::models::query::Column {
name: "name".to_string(),
display_name: "Name".to_string(),
base_type: "type/Text".to_string(),
effective_type: None,
semantic_type: None,
field_ref: None,
},
],
rows: vec![
vec![serde_json::json!(1), serde_json::json!("Test")],
vec![serde_json::json!(2), serde_json::json!("Sample")],
],
native_form: None,
insights: Vec::new(),
},
database_id: crate::core::models::common::MetabaseId(_database_id.0.into()),
started_at: chrono::Utc::now(),
finished_at: Some(chrono::Utc::now()),
json_query: serde_json::json!({}),
status: crate::core::models::query::QueryStatus::Completed,
row_count: Some(2),
running_time: Some(42),
}))
}
async fn execute_native_query(
&self,
database_id: i32,
query: crate::core::models::query::NativeQuery,
) -> RepositoryResult<crate::core::models::query::QueryResult> {
let mut params = std::collections::HashMap::new();
for (name, tag) in query.template_tags {
if let Some(default_value) = tag.default {
params.insert(name, default_value);
}
}
self.execute_native(
DatabaseId(database_id),
&query.query,
if params.is_empty() {
None
} else {
Some(params)
},
)
.await
}
async fn execute_mbql(
&self,
_database_id: DatabaseId,
_mbql: &serde_json::Value,
) -> RepositoryResult<crate::core::models::query::QueryResult> {
if self.should_fail {
return Err(RepositoryError::Other("Mock failure".to_string()));
}
self.execute_native(DatabaseId(1), "", None).await
}
async fn save_query(&self, query: &Query) -> RepositoryResult<Query> {
if self.should_fail {
return Err(RepositoryError::Other("Mock failure".to_string()));
}
let mut queries = self.queries.write().await;
let mut new_query = query.clone();
if new_query.id.is_none() {
new_query.id = Some((queries.len() + 1) as i32);
}
queries.push(new_query.clone());
Ok(new_query)
}
async fn get_query(&self, id: i32) -> RepositoryResult<Query> {
if self.should_fail {
return Err(RepositoryError::Other("Mock failure".to_string()));
}
let queries = self.queries.read().await;
queries
.iter()
.find(|q| q.id == Some(id))
.cloned()
.ok_or_else(|| RepositoryError::NotFound(format!("Query {} not found", id)))
}
async fn list_queries(
&self,
_pagination: Option<PaginationParams>,
filters: Option<QueryFilterParams>,
) -> RepositoryResult<Vec<Query>> {
if self.should_fail {
return Err(RepositoryError::Other("Mock failure".to_string()));
}
let queries = self.queries.read().await;
let mut result = queries.clone();
if let Some(f) = filters {
if let Some(db_id) = f.database_id {
result.retain(|q| q.database_id == db_id);
}
if let Some(qt) = f.query_type {
result.retain(|q| {
std::mem::discriminant(&q.query_type) == std::mem::discriminant(&qt)
});
}
}
Ok(result)
}
async fn update_query(&self, id: i32, query: &Query) -> RepositoryResult<Query> {
if self.should_fail {
return Err(RepositoryError::Other("Mock failure".to_string()));
}
let mut queries = self.queries.write().await;
if let Some(existing) = queries.iter_mut().find(|q| q.id == Some(id)) {
*existing = query.clone();
existing.id = Some(id); Ok(existing.clone())
} else {
Err(RepositoryError::NotFound(format!("Query {} not found", id)))
}
}
async fn delete_query(&self, id: i32) -> RepositoryResult<()> {
if self.should_fail {
return Err(RepositoryError::Other("Mock failure".to_string()));
}
let mut queries = self.queries.write().await;
let initial_len = queries.len();
queries.retain(|q| q.id != Some(id));
if queries.len() < initial_len {
Ok(())
} else {
Err(RepositoryError::NotFound(format!("Query {} not found", id)))
}
}
async fn get_metadata(&self, _database_id: DatabaseId) -> RepositoryResult<serde_json::Value> {
if self.should_fail {
return Err(RepositoryError::Other("Mock failure".to_string()));
}
Ok(serde_json::json!({
"tables": [
{
"name": "users",
"columns": [
{"name": "id", "type": "integer"},
{"name": "name", "type": "varchar"},
{"name": "email", "type": "varchar"},
]
},
{
"name": "orders",
"columns": [
{"name": "id", "type": "integer"},
{"name": "user_id", "type": "integer"},
{"name": "amount", "type": "decimal"},
]
}
]
}))
}
async fn validate_query(
&self,
_database_id: DatabaseId,
_query_type: QueryType,
_query: &serde_json::Value,
) -> RepositoryResult<bool> {
if self.should_fail {
return Err(RepositoryError::Other("Mock failure".to_string()));
}
Ok(true)
}
async fn get_execution_history(
&self,
_query_id: Option<i32>,
_limit: Option<u32>,
) -> RepositoryResult<Vec<serde_json::Value>> {
if self.should_fail {
return Err(RepositoryError::Other("Mock failure".to_string()));
}
Ok(vec![
serde_json::json!({
"id": 1,
"query_id": 1,
"executed_at": "2025-08-09T10:00:00Z",
"execution_time_ms": 42,
"rows_returned": 2,
}),
serde_json::json!({
"id": 2,
"query_id": 1,
"executed_at": "2025-08-09T09:00:00Z",
"execution_time_ms": 35,
"rows_returned": 3,
}),
])
}
}