diff --git a/crates/dataprof-db/src/connectors/sqlite.rs b/crates/dataprof-db/src/connectors/sqlite.rs index 6741a3b3..289dbcd1 100644 --- a/crates/dataprof-db/src/connectors/sqlite.rs +++ b/crates/dataprof-db/src/connectors/sqlite.rs @@ -16,6 +16,7 @@ use async_trait::async_trait; use {sqlx::sqlite::SqlitePool, sqlx::sqlite::SqlitePoolOptions}; /// SQLite embedded database connector +#[derive(Debug)] pub struct SqliteConnector { #[allow(dead_code)] config: DatabaseConfig, @@ -42,6 +43,30 @@ impl SqliteConnector { }); } + // SQLite has no network endpoint or credentials. Reject user, password, or port. + // A host by itself is allowed (e.g. sqlite://relative/path.db). + if connection_info.username.is_some() + || connection_info.password.is_some() + || connection_info.port.is_some() + { + let mut found = Vec::new(); + if connection_info.username.is_some() { + found.push("user"); + } + if connection_info.password.is_some() { + found.push("password"); + } + if connection_info.port.is_some() { + found.push("port"); + } + let found_str = found.join(", "); + return Err(DataProfilerError::DatabaseConfigError { + message: format!( + "SQLite URLs do not support {found_str}. Supported forms: sqlite:///absolute/path.db, sqlite://relative/path.db, sqlite::memory:, or a bare file path" + ), + }); + } + Ok(Self { config, connection_info, @@ -159,8 +184,7 @@ impl DatabaseConnector for SqliteConnector { use sqlx::Row; let pool = self.pool.as_ref().ok_or_else(not_connected_error)?; - - let query = format!("PRAGMA table_info({})", table_name); + let query = format!("PRAGMA table_info({table_name})"); let rows = sqlx::query(&query).fetch_all(pool).await.map_err(|e| { DataProfilerError::database_query(&format!("Failed to get table schema: {}", e)) @@ -188,14 +212,13 @@ impl DatabaseConnector for SqliteConnector { let pool = self.pool.as_ref().ok_or_else(not_connected_error)?; validate_sql_identifier(table_name)?; - let query = format!("SELECT COUNT(*) FROM {}", table_name); + let query = format!("SELECT COUNT(*) FROM {table_name}"); let count: i64 = sqlx::query_scalar(&query) .fetch_one(pool) .await .map_err(|e| { DataProfilerError::database_query(&format!("Failed to count rows: {}", e)) })?; - Ok(count as u64) } @@ -207,7 +230,6 @@ impl DatabaseConnector for SqliteConnector { #[cfg(feature = "sqlite")] { let pool = self.pool.as_ref().ok_or_else(not_connected_error)?; - let result: i32 = sqlx::query_scalar("SELECT 1") .fetch_one(pool) .await @@ -231,3 +253,62 @@ impl crate::DecodeStructured for sqlx::sqlite::SqliteRow { None } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::DatabaseConfig; + + fn config(url: &str) -> DatabaseConfig { + DatabaseConfig { + connection_string: url.to_string(), + ..Default::default() + } + } + + #[test] + fn rejects_user() { + let err = SqliteConnector::new(config("sqlite://user@host/data.db")).unwrap_err(); + let msg = err.to_string(); + assert!(msg.contains("user"), "{msg}"); + assert!(msg.contains("Supported forms"), "{msg}"); + } + + #[test] + fn rejects_password() { + let err = SqliteConnector::new(config("sqlite://:pw@host/data.db")).unwrap_err(); + let msg = err.to_string(); + assert!(msg.contains("password"), "{msg}"); + } + + #[test] + fn rejects_port() { + let err = SqliteConnector::new(config("sqlite://host:5432/data.db")).unwrap_err(); + let msg = err.to_string(); + assert!(msg.contains("port"), "{msg}"); + } + + #[test] + fn rejects_user_password_and_port() { + let err = SqliteConnector::new(config("sqlite://user:pw@host:5432/data.db")).unwrap_err(); + let msg = err.to_string(); + assert!(msg.contains("user"), "{msg}"); + assert!(msg.contains("password"), "{msg}"); + assert!(msg.contains("port"), "{msg}"); + // Must not include the actual password. + assert!(!msg.contains("pw"), "{msg}"); + } + + #[test] + fn accepts_supported_forms() { + for url in [ + "sqlite:///absolute/path.db", + "sqlite://relative/path.db", + "sqlite::memory:", + "/absolute/path.db", + "relative.db", + ] { + SqliteConnector::new(config(url)).unwrap_or_else(|e| panic!("should accept {url}: {e}")); + } + } +}