sbol_db_sqlite/
sql_console.rs1use std::time::{Duration, Instant};
12
13use async_trait::async_trait;
14use sbol_db_core::DomainError;
15use sbol_db_storage::{
16 SqlConsole, SqlConsoleColumn, SqlExecuteRequest, SqlExecuteResult, SqlValidateError,
17};
18use serde_json::Value;
19use sqlx::sqlite::SqliteRow;
20use sqlx::{Column, Executor, Row, TypeInfo};
21
22use crate::SqlitePool;
23
24#[derive(Clone)]
26pub struct SqliteSqlConsole {
27 pool: SqlitePool,
28}
29
30impl SqliteSqlConsole {
31 pub fn new(pool: SqlitePool) -> Self {
32 Self { pool }
33 }
34}
35
36fn value_to_json(row: &SqliteRow, idx: usize) -> Value {
40 if let Ok(Some(n)) = row.try_get::<Option<i64>, _>(idx) {
41 return Value::from(n);
42 }
43 if let Ok(Some(f)) = row.try_get::<Option<f64>, _>(idx) {
44 return serde_json::Number::from_f64(f)
45 .map(Value::Number)
46 .unwrap_or(Value::Null);
47 }
48 if let Ok(Some(s)) = row.try_get::<Option<String>, _>(idx) {
49 return Value::String(s);
50 }
51 if let Ok(Some(b)) = row.try_get::<Option<Vec<u8>>, _>(idx) {
52 return Value::String(hex::encode(b));
53 }
54 Value::Null
55}
56
57fn classify_execute_error(err: sqlx::Error) -> DomainError {
62 match err {
63 sqlx::Error::Database(db) => DomainError::InvalidInput(db.message().to_string()),
64 other => DomainError::Database(other.to_string()),
65 }
66}
67
68#[async_trait]
69impl SqlConsole for SqliteSqlConsole {
70 async fn execute(&self, req: SqlExecuteRequest) -> Result<SqlExecuteResult, DomainError> {
71 let started = Instant::now();
72 let fut = sqlx::query(&req.query).fetch_all(&self.pool);
73 let rows = match tokio::time::timeout(Duration::from_millis(req.timeout_ms), fut).await {
74 Ok(result) => result.map_err(classify_execute_error)?,
75 Err(_) => {
76 return Err(DomainError::InvalidInput(
77 "statement exceeded the time limit".to_string(),
78 ))
79 }
80 };
81 let elapsed_ms = started.elapsed().as_millis() as u64;
82
83 let total = rows.len() as u64;
84 let row_limit = req.row_limit as usize;
85 let truncated = total > req.row_limit as u64;
86
87 let columns: Vec<SqlConsoleColumn> = rows
88 .first()
89 .map(|row| {
90 row.columns()
91 .iter()
92 .map(|c| SqlConsoleColumn {
93 name: c.name().to_string(),
94 column_type: c.type_info().name().to_string(),
95 })
96 .collect()
97 })
98 .unwrap_or_default();
99
100 let json_rows: Vec<Vec<Value>> = rows
101 .iter()
102 .take(row_limit)
103 .map(|row| {
104 (0..row.columns().len())
105 .map(|i| value_to_json(row, i))
106 .collect()
107 })
108 .collect();
109
110 Ok(SqlExecuteResult {
111 columns,
112 rows: json_rows,
113 row_count: total,
114 truncated,
115 elapsed_ms,
116 backend_pid: None,
117 })
118 }
119
120 async fn validate(&self, query: &str) -> Result<Option<SqlValidateError>, DomainError> {
121 if query.trim().is_empty() {
122 return Ok(None);
123 }
124 let mut conn = self.pool.acquire().await.map_err(crate::pool::db_err)?;
125 match conn.prepare(query).await {
126 Ok(_) => Ok(None),
127 Err(err) => Ok(Some(SqlValidateError {
128 message: err.to_string(),
129 line: 1,
130 column: 1,
131 end_line: None,
132 end_column: None,
133 })),
134 }
135 }
136
137 fn dialect(&self) -> &'static str {
138 "sql"
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use crate::{connect, run_migrations};
146 use sbol_db_storage::SqlConsole;
147
148 async fn console() -> SqliteSqlConsole {
149 let pool = connect("sqlite::memory:").await.unwrap();
150 run_migrations(&pool).await.unwrap();
151 SqliteSqlConsole::new(pool)
152 }
153
154 fn req(query: &str, row_limit: u32) -> SqlExecuteRequest {
155 SqlExecuteRequest {
156 query: query.to_string(),
157 timeout_ms: 5_000,
158 row_limit,
159 }
160 }
161
162 #[tokio::test]
163 async fn execute_select_literal() {
164 let console = console().await;
165 let result = console.execute(req("SELECT 1 AS one", 100)).await.unwrap();
166 assert_eq!(result.columns.len(), 1);
167 assert_eq!(result.columns[0].name, "one");
168 assert_eq!(result.rows.len(), 1);
169 assert_eq!(result.rows[0][0], Value::from(1));
170 assert_eq!(result.row_count, 1);
171 assert!(!result.truncated);
172 assert!(result.backend_pid.is_none());
173 }
174
175 #[tokio::test]
176 async fn execute_applies_row_limit() {
177 let console = console().await;
178 let result = console
179 .execute(req("SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3", 2))
180 .await
181 .unwrap();
182 assert_eq!(result.rows.len(), 2);
183 assert_eq!(result.row_count, 3);
184 assert!(result.truncated);
185 }
186
187 #[tokio::test]
188 async fn execute_bad_sql_is_invalid_input() {
189 let console = console().await;
190 let err = console.execute(req("SELCT 1", 100)).await.unwrap_err();
191 assert!(matches!(err, DomainError::InvalidInput(_)));
192 }
193
194 #[tokio::test]
195 async fn validate_reports_syntax_error() {
196 let console = console().await;
197 let err = console.validate("SELCT 1").await.unwrap();
198 assert!(err.is_some());
199 let err = err.unwrap();
200 assert_eq!(err.line, 1);
201 assert_eq!(err.column, 1);
202 }
203
204 #[tokio::test]
205 async fn validate_accepts_valid_sql() {
206 let console = console().await;
207 assert!(console.validate("SELECT 1").await.unwrap().is_none());
208 assert!(console.validate(" ").await.unwrap().is_none());
209 }
210
211 #[tokio::test]
212 async fn dialect_is_sql() {
213 let console = console().await;
214 assert_eq!(console.dialect(), "sql");
215 }
216}