use axum::{
extract::{Path, State},
http::StatusCode,
Json,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use super::handlers::AppState;
use crate::sql::translate_sql_to_sdbql;
#[derive(Debug, Deserialize)]
pub struct SqlRequest {
pub query: String,
#[serde(default)]
pub bind_vars: HashMap<String, Value>,
#[serde(default)]
pub dry_run: bool,
}
#[derive(Debug, Serialize)]
pub struct SqlResponse {
pub result: Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub sdbql: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct SqlTranslateResponse {
pub sdbql: String,
pub sql: String,
}
pub async fn execute_sql_handler(
State(state): State<AppState>,
Path(db): Path<String>,
axum::Extension(claims): axum::Extension<crate::server::auth::Claims>,
Json(request): Json<SqlRequest>,
) -> Result<Json<SqlResponse>, (StatusCode, Json<SqlResponse>)> {
let sdbql = match translate_sql_to_sdbql(&request.query) {
Ok(s) => s,
Err(e) => {
return Err((
StatusCode::BAD_REQUEST,
Json(SqlResponse {
result: Value::Null,
sdbql: None,
error: Some(format!("SQL parse error: {}", e)),
}),
));
}
};
if request.dry_run {
return Ok(Json(SqlResponse {
result: Value::Null,
sdbql: Some(sdbql),
error: None,
}));
}
let query_ast = match crate::sdbql::parse(&sdbql) {
Ok(ast) => ast,
Err(e) => {
return Err((
StatusCode::BAD_REQUEST,
Json(SqlResponse {
result: Value::Null,
sdbql: Some(sdbql),
error: Some(format!("SDBQL parse error: {}", e)),
}),
));
}
};
if query_ast.has_mutations() {
if let Err(e) = crate::server::authz_middleware::enforce(
&claims,
&state,
crate::server::authorization::PermissionAction::Write,
Some(&db),
)
.await
{
return Err((
StatusCode::FORBIDDEN,
Json(SqlResponse {
result: Value::Null,
sdbql: Some(sdbql),
error: Some(e.to_string()),
}),
));
}
}
let mut executor = crate::sdbql::QueryExecutor::with_database_and_bind_vars(
&state.storage,
db.clone(),
request.bind_vars,
)
.with_principal(crate::server::handlers::query::principal_from_claims(
&claims,
));
if let Some(ref log) = state.replication_log {
executor = executor.with_replication(log);
}
match executor.execute(&query_ast) {
Ok(results) => Ok(Json(SqlResponse {
result: Value::Array(results),
sdbql: Some(sdbql),
error: None,
})),
Err(e) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(SqlResponse {
result: Value::Null,
sdbql: Some(sdbql),
error: Some(format!("Query execution error: {}", e)),
}),
)),
}
}
pub async fn translate_sql_handler(
Json(request): Json<SqlRequest>,
) -> Result<Json<SqlTranslateResponse>, (StatusCode, Json<SqlResponse>)> {
match translate_sql_to_sdbql(&request.query) {
Ok(sdbql) => Ok(Json(SqlTranslateResponse {
sdbql,
sql: request.query,
})),
Err(e) => Err((
StatusCode::BAD_REQUEST,
Json(SqlResponse {
result: Value::Null,
sdbql: None,
error: Some(format!("SQL parse error: {}", e)),
}),
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sql_translate() {
let result = translate_sql_to_sdbql("SELECT * FROM users").unwrap();
assert!(result.contains("FOR doc IN users"));
assert!(result.contains("RETURN doc"));
}
#[test]
fn test_sql_with_where() {
let result = translate_sql_to_sdbql("SELECT * FROM users WHERE age > 18").unwrap();
assert!(result.contains("FILTER doc.age > 18"));
}
}