use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[cfg(feature = "duckdb-backend")]
pub mod duckdb;
#[cfg(feature = "postgres-backend")]
pub mod postgres;
pub mod config;
pub mod schema;
pub mod sync;
#[cfg(feature = "duckdb-backend")]
pub use self::duckdb::DuckDBBackend;
#[cfg(feature = "postgres-backend")]
pub use self::postgres::PostgresBackend;
pub use config::DatabaseConfig;
pub use schema::DatabaseSchema;
pub use sync::{SyncEngine, SyncResult};
#[derive(Debug, thiserror::Error)]
pub enum DatabaseError {
#[error("Connection failed: {0}")]
ConnectionFailed(String),
#[error("Query failed: {0}")]
QueryFailed(String),
#[error("Sync failed: {0}")]
SyncFailed(String),
#[error("Migration failed: {0}")]
MigrationFailed(String),
#[error("Transaction failed: {0}")]
TransactionFailed(String),
#[error("Serialization error: {0}")]
SerializationError(String),
#[error("Configuration error: {0}")]
ConfigError(String),
#[error("Database not initialized. Run 'db init' first.")]
NotInitialized,
#[error("Workspace not found: {0}")]
WorkspaceNotFound(String),
#[error("Invalid input: {0}")]
InvalidInput(String),
#[error("IO error: {0}")]
IoError(String),
}
pub type DatabaseResult<T> = Result<T, DatabaseError>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SyncStatus {
pub workspace_id: Uuid,
pub last_sync_at: Option<chrono::DateTime<chrono::Utc>>,
pub table_count: usize,
pub column_count: usize,
pub relationship_count: usize,
pub domain_count: usize,
pub decision_count: usize,
pub knowledge_count: usize,
pub is_stale: bool,
pub pending_sync_count: usize,
}
impl Default for SyncStatus {
fn default() -> Self {
Self {
workspace_id: Uuid::nil(),
last_sync_at: None,
table_count: 0,
column_count: 0,
relationship_count: 0,
domain_count: 0,
decision_count: 0,
knowledge_count: 0,
is_stale: true,
pending_sync_count: 0,
}
}
}
pub type QueryRow = serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QueryResult {
pub columns: Vec<String>,
pub rows: Vec<QueryRow>,
pub rows_affected: Option<u64>,
pub execution_time_ms: u64,
}
impl QueryResult {
pub fn new(columns: Vec<String>, rows: Vec<QueryRow>) -> Self {
Self {
columns,
rows,
rows_affected: None,
execution_time_ms: 0,
}
}
pub fn empty() -> Self {
Self {
columns: Vec::new(),
rows: Vec::new(),
rows_affected: None,
execution_time_ms: 0,
}
}
pub fn row_count(&self) -> usize {
self.rows.len()
}
pub fn is_empty(&self) -> bool {
self.rows.is_empty()
}
}
#[async_trait(?Send)]
pub trait DatabaseBackend: Send + Sync {
async fn initialize(&self) -> DatabaseResult<()>;
async fn execute_query(&self, sql: &str) -> DatabaseResult<QueryResult>;
async fn execute_query_params(
&self,
sql: &str,
params: &[serde_json::Value],
) -> DatabaseResult<QueryResult>;
async fn sync_tables(
&self,
workspace_id: Uuid,
tables: &[crate::models::Table],
) -> DatabaseResult<usize>;
async fn sync_domains(
&self,
workspace_id: Uuid,
domains: &[crate::models::Domain],
) -> DatabaseResult<usize>;
async fn sync_relationships(
&self,
workspace_id: Uuid,
relationships: &[crate::models::Relationship],
) -> DatabaseResult<usize>;
async fn export_tables(&self, workspace_id: Uuid) -> DatabaseResult<Vec<crate::models::Table>>;
async fn export_domains(
&self,
workspace_id: Uuid,
) -> DatabaseResult<Vec<crate::models::Domain>>;
async fn export_relationships(
&self,
workspace_id: Uuid,
) -> DatabaseResult<Vec<crate::models::Relationship>>;
async fn sync_decisions(
&self,
workspace_id: Uuid,
decisions: &[crate::models::decision::Decision],
) -> DatabaseResult<usize>;
async fn sync_knowledge(
&self,
workspace_id: Uuid,
articles: &[crate::models::knowledge::KnowledgeArticle],
) -> DatabaseResult<usize>;
async fn export_decisions(
&self,
workspace_id: Uuid,
) -> DatabaseResult<Vec<crate::models::decision::Decision>>;
async fn export_knowledge(
&self,
workspace_id: Uuid,
) -> DatabaseResult<Vec<crate::models::knowledge::KnowledgeArticle>>;
async fn get_sync_status(&self, workspace_id: Uuid) -> DatabaseResult<SyncStatus>;
async fn upsert_workspace(&self, workspace: &crate::models::Workspace) -> DatabaseResult<()>;
async fn get_workspace(
&self,
workspace_id: Uuid,
) -> DatabaseResult<Option<crate::models::Workspace>>;
async fn get_workspace_by_name(
&self,
name: &str,
) -> DatabaseResult<Option<crate::models::Workspace>>;
async fn delete_workspace(&self, workspace_id: Uuid) -> DatabaseResult<()>;
async fn record_file_hash(
&self,
workspace_id: Uuid,
file_path: &str,
hash: &str,
) -> DatabaseResult<()>;
async fn get_file_hash(
&self,
workspace_id: Uuid,
file_path: &str,
) -> DatabaseResult<Option<String>>;
async fn health_check(&self) -> DatabaseResult<bool>;
fn backend_type(&self) -> &'static str;
async fn close(&self) -> DatabaseResult<()>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum OutputFormat {
#[default]
Table,
Json,
Csv,
}
impl std::str::FromStr for OutputFormat {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"table" => Ok(OutputFormat::Table),
"json" => Ok(OutputFormat::Json),
"csv" => Ok(OutputFormat::Csv),
_ => Err(format!("Unknown output format: {}", s)),
}
}
}
impl std::fmt::Display for OutputFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
OutputFormat::Table => write!(f, "table"),
OutputFormat::Json => write!(f, "json"),
OutputFormat::Csv => write!(f, "csv"),
}
}
}
pub fn format_query_result(result: &QueryResult, format: OutputFormat) -> String {
match format {
OutputFormat::Json => {
serde_json::to_string_pretty(&result.rows).unwrap_or_else(|_| "[]".to_string())
}
OutputFormat::Csv => format_as_csv(result),
OutputFormat::Table => format_as_table(result),
}
}
fn format_as_csv(result: &QueryResult) -> String {
let mut output = String::new();
output.push_str(&result.columns.join(","));
output.push('\n');
for row in &result.rows {
let values: Vec<String> = result
.columns
.iter()
.map(|col| {
let value = row.get(col).unwrap_or(&serde_json::Value::Null);
match value {
serde_json::Value::String(s) => {
if s.contains(',') || s.contains('"') || s.contains('\n') {
format!("\"{}\"", s.replace('"', "\"\""))
} else {
s.clone()
}
}
serde_json::Value::Null => String::new(),
other => other.to_string(),
}
})
.collect();
output.push_str(&values.join(","));
output.push('\n');
}
output
}
fn format_as_table(result: &QueryResult) -> String {
if result.is_empty() {
return "(0 rows)".to_string();
}
let mut widths: Vec<usize> = result.columns.iter().map(|c| c.len()).collect();
for row in &result.rows {
for (i, col) in result.columns.iter().enumerate() {
let value = row.get(col).unwrap_or(&serde_json::Value::Null);
let len = match value {
serde_json::Value::String(s) => s.len(),
serde_json::Value::Null => 4, other => other.to_string().len(),
};
widths[i] = widths[i].max(len);
}
}
let mut output = String::new();
let header: Vec<String> = result
.columns
.iter()
.enumerate()
.map(|(i, c)| format!("{:width$}", c, width = widths[i]))
.collect();
output.push_str(&header.join(" | "));
output.push('\n');
let separator: Vec<String> = widths.iter().map(|w| "-".repeat(*w)).collect();
output.push_str(&separator.join("-+-"));
output.push('\n');
for row in &result.rows {
let values: Vec<String> = result
.columns
.iter()
.enumerate()
.map(|(i, col)| {
let value = row.get(col).unwrap_or(&serde_json::Value::Null);
let s = match value {
serde_json::Value::String(s) => s.clone(),
serde_json::Value::Null => "null".to_string(),
other => other.to_string(),
};
format!("{:width$}", s, width = widths[i])
})
.collect();
output.push_str(&values.join(" | "));
output.push('\n');
}
output.push_str(&format!("({} rows)", result.row_count()));
output
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
#[test]
fn test_output_format_from_str() {
assert_eq!(
OutputFormat::from_str("table").unwrap(),
OutputFormat::Table
);
assert_eq!(OutputFormat::from_str("json").unwrap(), OutputFormat::Json);
assert_eq!(OutputFormat::from_str("csv").unwrap(), OutputFormat::Csv);
assert_eq!(OutputFormat::from_str("JSON").unwrap(), OutputFormat::Json);
assert!(OutputFormat::from_str("unknown").is_err());
}
#[test]
fn test_query_result_empty() {
let result = QueryResult::empty();
assert!(result.is_empty());
assert_eq!(result.row_count(), 0);
}
#[test]
fn test_format_as_table() {
let result = QueryResult::new(
vec!["name".to_string(), "count".to_string()],
vec![
serde_json::json!({"name": "users", "count": 10}),
serde_json::json!({"name": "orders", "count": 100}),
],
);
let output = format_as_table(&result);
assert!(output.contains("name"));
assert!(output.contains("count"));
assert!(output.contains("users"));
assert!(output.contains("(2 rows)"));
}
#[test]
fn test_format_as_csv() {
let result = QueryResult::new(
vec!["name".to_string(), "description".to_string()],
vec![
serde_json::json!({"name": "test", "description": "simple"}),
serde_json::json!({"name": "complex", "description": "has, comma"}),
],
);
let output = format_as_csv(&result);
assert!(output.contains("name,description"));
assert!(output.contains("test,simple"));
assert!(output.contains("\"has, comma\"")); }
#[test]
fn test_sync_status_default() {
let status = SyncStatus::default();
assert!(status.is_stale);
assert_eq!(status.table_count, 0);
assert!(status.last_sync_at.is_none());
}
}