use std::time::{Duration, Instant};
use async_trait::async_trait;
use sbol_db_core::DomainError;
use sbol_db_storage::{
SqlConsole, SqlConsoleColumn, SqlExecuteRequest, SqlExecuteResult, SqlValidateError,
};
use serde_json::Value;
use sqlx::sqlite::SqliteRow;
use sqlx::{Column, Executor, Row, TypeInfo};
use crate::SqlitePool;
#[derive(Clone)]
pub struct SqliteSqlConsole {
pool: SqlitePool,
}
impl SqliteSqlConsole {
pub fn new(pool: SqlitePool) -> Self {
Self { pool }
}
}
fn value_to_json(row: &SqliteRow, idx: usize) -> Value {
if let Ok(Some(n)) = row.try_get::<Option<i64>, _>(idx) {
return Value::from(n);
}
if let Ok(Some(f)) = row.try_get::<Option<f64>, _>(idx) {
return serde_json::Number::from_f64(f)
.map(Value::Number)
.unwrap_or(Value::Null);
}
if let Ok(Some(s)) = row.try_get::<Option<String>, _>(idx) {
return Value::String(s);
}
if let Ok(Some(b)) = row.try_get::<Option<Vec<u8>>, _>(idx) {
return Value::String(hex::encode(b));
}
Value::Null
}
fn classify_execute_error(err: sqlx::Error) -> DomainError {
match err {
sqlx::Error::Database(db) => DomainError::InvalidInput(db.message().to_string()),
other => DomainError::Database(other.to_string()),
}
}
#[async_trait]
impl SqlConsole for SqliteSqlConsole {
async fn execute(&self, req: SqlExecuteRequest) -> Result<SqlExecuteResult, DomainError> {
let started = Instant::now();
let fut = sqlx::query(&req.query).fetch_all(&self.pool);
let rows = match tokio::time::timeout(Duration::from_millis(req.timeout_ms), fut).await {
Ok(result) => result.map_err(classify_execute_error)?,
Err(_) => {
return Err(DomainError::InvalidInput(
"statement exceeded the time limit".to_string(),
))
}
};
let elapsed_ms = started.elapsed().as_millis() as u64;
let total = rows.len() as u64;
let row_limit = req.row_limit as usize;
let truncated = total > req.row_limit as u64;
let columns: Vec<SqlConsoleColumn> = rows
.first()
.map(|row| {
row.columns()
.iter()
.map(|c| SqlConsoleColumn {
name: c.name().to_string(),
column_type: c.type_info().name().to_string(),
})
.collect()
})
.unwrap_or_default();
let json_rows: Vec<Vec<Value>> = rows
.iter()
.take(row_limit)
.map(|row| {
(0..row.columns().len())
.map(|i| value_to_json(row, i))
.collect()
})
.collect();
Ok(SqlExecuteResult {
columns,
rows: json_rows,
row_count: total,
truncated,
elapsed_ms,
backend_pid: None,
})
}
async fn validate(&self, query: &str) -> Result<Option<SqlValidateError>, DomainError> {
if query.trim().is_empty() {
return Ok(None);
}
let mut conn = self.pool.acquire().await.map_err(crate::pool::db_err)?;
match conn.prepare(query).await {
Ok(_) => Ok(None),
Err(err) => Ok(Some(SqlValidateError {
message: err.to_string(),
line: 1,
column: 1,
end_line: None,
end_column: None,
})),
}
}
fn dialect(&self) -> &'static str {
"sql"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{connect, run_migrations};
use sbol_db_storage::SqlConsole;
async fn console() -> SqliteSqlConsole {
let pool = connect("sqlite::memory:").await.unwrap();
run_migrations(&pool).await.unwrap();
SqliteSqlConsole::new(pool)
}
fn req(query: &str, row_limit: u32) -> SqlExecuteRequest {
SqlExecuteRequest {
query: query.to_string(),
timeout_ms: 5_000,
row_limit,
}
}
#[tokio::test]
async fn execute_select_literal() {
let console = console().await;
let result = console.execute(req("SELECT 1 AS one", 100)).await.unwrap();
assert_eq!(result.columns.len(), 1);
assert_eq!(result.columns[0].name, "one");
assert_eq!(result.rows.len(), 1);
assert_eq!(result.rows[0][0], Value::from(1));
assert_eq!(result.row_count, 1);
assert!(!result.truncated);
assert!(result.backend_pid.is_none());
}
#[tokio::test]
async fn execute_applies_row_limit() {
let console = console().await;
let result = console
.execute(req("SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3", 2))
.await
.unwrap();
assert_eq!(result.rows.len(), 2);
assert_eq!(result.row_count, 3);
assert!(result.truncated);
}
#[tokio::test]
async fn execute_bad_sql_is_invalid_input() {
let console = console().await;
let err = console.execute(req("SELCT 1", 100)).await.unwrap_err();
assert!(matches!(err, DomainError::InvalidInput(_)));
}
#[tokio::test]
async fn validate_reports_syntax_error() {
let console = console().await;
let err = console.validate("SELCT 1").await.unwrap();
assert!(err.is_some());
let err = err.unwrap();
assert_eq!(err.line, 1);
assert_eq!(err.column, 1);
}
#[tokio::test]
async fn validate_accepts_valid_sql() {
let console = console().await;
assert!(console.validate("SELECT 1").await.unwrap().is_none());
assert!(console.validate(" ").await.unwrap().is_none());
}
#[tokio::test]
async fn dialect_is_sql() {
let console = console().await;
assert_eq!(console.dialect(), "sql");
}
}