use crate::error::OrmError;
use serde::{Deserialize, Serialize};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tokio::time::interval;
/// Theme configuration for visualizations
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Theme {
pub name: String,
pub primary_color: String,
pub secondary_color: String,
pub background_color: String,
pub text_color: String,
pub border_color: String,
pub font_family: String,
pub font_size: u32,
}
impl Default for Theme {
fn default() -> Self {
Self {
name: "default".to_string(),
primary_color: "#2E86AB".to_string(),
secondary_color: "#A23B72".to_string(),
background_color: "#F8F9FA".to_string(),
text_color: "#212529".to_string(),
border_color: "#DEE2E6".to_string(),
font_family: "Arial, sans-serif".to_string(),
font_size: 14,
}
}
}
/// Database column information
#[derive(Debug, Clone, Serialize)]
pub struct Column {
pub name: String,
pub data_type: String,
pub is_nullable: bool,
pub is_primary_key: bool,
pub is_foreign_key: bool,
}
/// Database table information
#[derive(Debug, Clone, Serialize)]
pub struct Table {
pub name: String,
pub columns: Vec<Column>,
}
/// Database relationship information
#[derive(Debug, Clone, Serialize)]
pub struct Relationship {
pub from_table: String,
pub from_column: String,
pub to_table: String,
pub to_column: String,
pub relationship_type: String,
}
/// Schema visualization configuration
#[derive(Debug, Clone)]
pub struct VisualizationConfig {
pub theme: Theme,
pub show_relationships: bool,
pub show_data_types: bool,
pub show_constraints: bool,
pub layout_engine: String,
}
impl Default for VisualizationConfig {
fn default() -> Self {
Self {
theme: Theme::default(),
show_relationships: true,
show_data_types: true,
show_constraints: true,
layout_engine: "dot".to_string(),
}
}
}
/// Real-time schema monitoring configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MonitoringConfig {
pub check_interval_seconds: u64,
pub enable_notifications: bool,
pub track_schema_changes: bool,
pub track_table_changes: bool,
pub track_column_changes: bool,
pub track_relationship_changes: bool,
}
impl Default for MonitoringConfig {
fn default() -> Self {
Self {
check_interval_seconds: 30,
enable_notifications: true,
track_schema_changes: true,
track_table_changes: true,
track_column_changes: true,
track_relationship_changes: true,
}
}
}
/// Schema change event
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SchemaChangeEvent {
pub timestamp: chrono::DateTime<chrono::Utc>,
pub event_type: String,
pub table_name: Option<String>,
pub column_name: Option<String>,
pub change_description: String,
pub severity: String,
}
/// Real-time schema monitor
pub struct SchemaMonitor {
config: MonitoringConfig,
last_schema: Arc<Mutex<Option<Vec<Table>>>>,
last_relationships: Arc<Mutex<Option<Vec<Relationship>>>>,
change_history: Arc<Mutex<Vec<SchemaChangeEvent>>>,
is_monitoring: Arc<Mutex<bool>>,
}
impl SchemaMonitor {
pub fn new(config: MonitoringConfig) -> Self {
Self {
config,
last_schema: Arc::new(Mutex::new(None)),
last_relationships: Arc::new(Mutex::new(None)),
change_history: Arc::new(Mutex::new(Vec::new())),
is_monitoring: Arc::new(Mutex::new(false)),
}
}
/// Start monitoring the schema for changes
pub async fn start_monitoring<F>(&self, database_url: &str, callback: F) -> Result<(), OrmError>
where
F: Fn(SchemaChangeEvent) + Send + Sync + 'static,
{
let mut is_monitoring = self.is_monitoring.lock().unwrap();
if *is_monitoring {
return Ok(());
}
*is_monitoring = true;
drop(is_monitoring);
let config = self.config.clone();
let last_schema = Arc::clone(&self.last_schema);
let last_relationships = Arc::clone(&self.last_relationships);
let change_history = Arc::clone(&self.change_history);
let is_monitoring = Arc::clone(&self.is_monitoring);
let database_url = database_url.to_string();
tokio::spawn(async move {
let mut interval = interval(Duration::from_secs(config.check_interval_seconds));
while {
let is_monitoring_guard = is_monitoring.lock().unwrap();
*is_monitoring_guard
} {
interval.tick().await;
if let Err(e) = Self::check_schema_changes(
&database_url,
&config,
&last_schema,
&last_relationships,
&change_history,
&callback,
)
.await
{
eprintln!("Schema monitoring error: {}", e);
}
}
});
Ok(())
}
/// Stop monitoring the schema
pub fn stop_monitoring(&self) {
let mut is_monitoring = self.is_monitoring.lock().unwrap();
*is_monitoring = false;
}
/// Check for schema changes
async fn check_schema_changes<F>(
_database_url: &str,
config: &MonitoringConfig,
_last_schema: &Arc<Mutex<Option<Vec<Table>>>>,
_last_relationships: &Arc<Mutex<Option<Vec<Relationship>>>>,
change_history: &Arc<Mutex<Vec<SchemaChangeEvent>>>,
callback: &F,
) -> Result<(), OrmError>
where
F: Fn(SchemaChangeEvent) + Send + Sync,
{
// This would connect to the database and check for changes
// For now, we'll simulate schema checking
let current_time = chrono::Utc::now();
// Simulate checking for changes
if config.track_schema_changes {
let event = SchemaChangeEvent {
timestamp: current_time,
event_type: "schema_check".to_string(),
table_name: None,
column_name: None,
change_description: "Schema check completed".to_string(),
severity: "info".to_string(),
};
callback(event.clone());
// Store in history
let mut history = change_history.lock().unwrap();
history.push(event);
// Keep only last 100 events
if history.len() > 100 {
let len = history.len();
if len > 100 {
history.drain(0..len - 100);
}
}
}
Ok(())
}
/// Get change history
pub fn get_change_history(&self) -> Vec<SchemaChangeEvent> {
let history = self.change_history.lock().unwrap();
history.clone()
}
/// Get monitoring status
pub fn is_monitoring(&self) -> bool {
let is_monitoring = self.is_monitoring.lock().unwrap();
*is_monitoring
}
}
/// Enhanced schema visualizer with advanced features
pub struct SchemaVisualizer {
tables: Vec<Table>,
relationships: Vec<Relationship>,
config: VisualizationConfig,
}
impl SchemaVisualizer {
pub fn new(tables: Vec<Table>, relationships: Vec<Relationship>) -> Self {
Self {
tables,
relationships,
config: VisualizationConfig::default(),
}
}
pub fn with_config(mut self, config: VisualizationConfig) -> Self {
self.config = config;
self
}
pub fn with_theme(mut self, theme: Theme) -> Self {
self.config.theme = theme;
self
}
/// Generate Graphviz DOT format with enhanced styling
pub fn generate_dot(&self) -> Result<String, OrmError> {
let mut dot = String::new();
dot.push_str("digraph DatabaseSchema {\n");
dot.push_str(" rankdir=LR;\n");
dot.push_str(&format!(" node [shape=record, style=filled, fillcolor=\"{}\", fontname=\"{}\", fontsize={}];\n",
self.config.theme.background_color, self.config.theme.font_family, self.config.theme.font_size));
dot.push_str(&format!(
" edge [color=\"{}\", fontname=\"{}\", fontsize={}];\n",
self.config.theme.border_color,
self.config.theme.font_family,
self.config.theme.font_size
));
// Add tables
for table in &self.tables {
dot.push_str(&format!(" \"{}\" [label=\"{{{}", table.name, table.name));
if self.config.show_data_types {
for column in &table.columns {
let mut column_label = column.name.clone();
if self.config.show_constraints {
if column.is_primary_key {
column_label.push_str(" (PK)");
}
if column.is_foreign_key {
column_label.push_str(" (FK)");
}
if !column.is_nullable {
column_label.push_str(" NOT NULL");
}
}
column_label.push_str(&format!(": {}", column.data_type));
dot.push_str(&format!("|{}", column_label));
}
}
dot.push_str("}}\"];\n");
}
// Add relationships
if self.config.show_relationships {
for rel in &self.relationships {
dot.push_str(&format!(
" \"{}\":\"{}\" -> \"{}\":\"{}\" [label=\"{}\"];\n",
rel.from_table,
rel.from_column,
rel.to_table,
rel.to_column,
rel.relationship_type
));
}
}
dot.push_str("}\n");
Ok(dot)
}
/// Generate Mermaid format with enhanced styling
pub fn generate_mermaid(&self) -> Result<String, OrmError> {
let mut mermaid = String::new();
mermaid.push_str("erDiagram\n");
for table in &self.tables {
mermaid.push_str(&format!(" {} {{\n", table.name));
for column in &table.columns {
let mut column_line = format!(" {}", column.name);
if self.config.show_data_types {
column_line.push_str(&format!(" {}", column.data_type));
}
if self.config.show_constraints {
if column.is_primary_key {
column_line.push_str(" PK");
}
if column.is_foreign_key {
column_line.push_str(" FK");
}
if !column.is_nullable {
column_line.push_str(" NOT NULL");
}
}
mermaid.push_str(&format!("{}\n", column_line));
}
mermaid.push_str(" }\n");
}
if self.config.show_relationships {
for rel in &self.relationships {
mermaid.push_str(&format!(
" {} ||--o{{ {} : \"{}\"\n",
rel.from_table, rel.to_table, rel.relationship_type
));
}
}
Ok(mermaid)
}
/// Generate enhanced HTML with interactive features
pub fn generate_html(&self) -> Result<String, OrmError> {
let mut html = String::new();
html.push_str("<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n");
html.push_str(" <meta charset=\"UTF-8\">\n");
html.push_str(
" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n",
);
html.push_str(" <title>Database Schema Visualization</title>\n");
html.push_str(" <style>\n");
html.push_str(&format!(
" body {{ font-family: {}; margin: 0; padding: 20px; background-color: {}; }}\n",
self.config.theme.font_family, self.config.theme.background_color
));
html.push_str(" .container { max-width: 1200px; margin: 0 auto; }\n");
html.push_str(" .header { text-align: center; margin-bottom: 30px; }\n");
html.push_str(&format!(
" .header h1 {{ color: {}; }}\n",
self.config.theme.primary_color
));
html.push_str(" .table-container { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; }\n");
html.push_str(&format!(" .table-card {{ background: white; border: 2px solid {}; border-radius: 8px; padding: 15px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }}\n",
self.config.theme.border_color));
html.push_str(&format!(" .table-name {{ font-size: 18px; font-weight: bold; color: {}; margin-bottom: 15px; border-bottom: 2px solid {}; padding-bottom: 5px; }}\n",
self.config.theme.primary_color, self.config.theme.border_color));
html.push_str(" .column { padding: 5px 0; border-bottom: 1px solid #eee; }\n");
html.push_str(" .column:last-child { border-bottom: none; }\n");
html.push_str(&format!(
" .pk {{ color: {}; font-weight: bold; }}\n",
self.config.theme.primary_color
));
html.push_str(&format!(
" .fk {{ color: {}; font-weight: bold; }}\n",
self.config.theme.secondary_color
));
html.push_str(" .not-null { font-style: italic; }\n");
html.push_str(" .controls { margin-bottom: 20px; text-align: center; }\n");
html.push_str(&format!(" .btn {{ background: {}; color: white; border: none; padding: 10px 20px; margin: 0 5px; border-radius: 5px; cursor: pointer; }}\n",
self.config.theme.primary_color));
html.push_str(" .btn:hover { opacity: 0.8; }\n");
html.push_str(&format!(" .search {{ padding: 8px; border: 1px solid {}; border-radius: 4px; width: 200px; margin-right: 10px; }}\n",
self.config.theme.border_color));
html.push_str(" .zoom-controls { position: fixed; top: 20px; right: 20px; background: white; padding: 10px; border-radius: 5px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }\n");
html.push_str(&format!(" .zoom-btn {{ background: {}; color: white; border: none; padding: 5px 10px; margin: 2px; border-radius: 3px; cursor: pointer; }}\n",
self.config.theme.secondary_color));
html.push_str(" </style>\n");
html.push_str("</head>\n<body>\n");
// Controls
html.push_str(" <div class=\"controls\">\n");
html.push_str(" <input type=\"text\" class=\"search\" id=\"searchInput\" placeholder=\"Search tables...\">\n");
html.push_str(
" <button class=\"btn\" onclick=\"exportSVG()\">Export SVG</button>\n",
);
html.push_str(
" <button class=\"btn\" onclick=\"exportPDF()\">Export PDF</button>\n",
);
html.push_str(
" <button class=\"btn\" onclick=\"toggleTheme()\">Toggle Theme</button>\n",
);
html.push_str(" </div>\n");
// Zoom controls
html.push_str(" <div class=\"zoom-controls\">\n");
html.push_str(" <button class=\"zoom-btn\" onclick=\"zoomIn()\">+</button><br>\n");
html.push_str(" <button class=\"zoom-btn\" onclick=\"zoomOut()\">-</button><br>\n");
html.push_str(
" <button class=\"zoom-btn\" onclick=\"resetZoom()\">Reset</button>\n",
);
html.push_str(" </div>\n");
html.push_str(" <div class=\"container\">\n");
html.push_str(" <div class=\"header\">\n");
html.push_str(" <h1>Database Schema Visualization</h1>\n");
html.push_str(&format!(
" <p>Generated on {}</p>\n",
chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC")
));
html.push_str(" </div>\n");
html.push_str(" <div class=\"table-container\" id=\"tableContainer\">\n");
for table in &self.tables {
html.push_str(&format!(
" <div class=\"table-card\" data-table-name=\"{}\">\n",
table.name
));
html.push_str(&format!(
" <div class=\"table-name\">{}</div>\n",
table.name
));
for column in &table.columns {
let mut column_classes = Vec::new();
if column.is_primary_key {
column_classes.push("pk");
}
if column.is_foreign_key {
column_classes.push("fk");
}
if !column.is_nullable {
column_classes.push("not-null");
}
let class_attr = if column_classes.is_empty() {
"".to_string()
} else {
format!(" class=\"{}\"", column_classes.join(" "))
};
html.push_str(&format!(
" <div class=\"column\"{}>\n",
class_attr
));
html.push_str(&format!(
" <strong>{}</strong>: {}\n",
column.name, column.data_type
));
if self.config.show_constraints {
let mut constraints = Vec::new();
if column.is_primary_key {
constraints.push("Primary Key");
}
if column.is_foreign_key {
constraints.push("Foreign Key");
}
if !column.is_nullable {
constraints.push("Not Null");
}
if !constraints.is_empty() {
html.push_str(&format!(
" <br><small>({})</small>\n",
constraints.join(", ")
));
}
}
html.push_str(" </div>\n");
}
html.push_str(" </div>\n");
}
html.push_str(" </div>\n");
html.push_str(" </div>\n");
// JavaScript for interactivity
html.push_str(" <script>\n");
html.push_str(" let currentZoom = 1;\n");
html.push_str(" let currentTheme = 'default';\n");
html.push_str(" \n");
html.push_str(" // Search functionality\n");
html.push_str(" document.getElementById('searchInput').addEventListener('input', function(e) {\n");
html.push_str(" const searchTerm = e.target.value.toLowerCase();\n");
html.push_str(" const tables = document.querySelectorAll('.table-card');\n");
html.push_str(" \n");
html.push_str(" tables.forEach(table => {\n");
html.push_str(" const tableName = table.getAttribute('data-table-name').toLowerCase();\n");
html.push_str(" if (tableName.includes(searchTerm)) {\n");
html.push_str(" table.style.display = 'block';\n");
html.push_str(" } else {\n");
html.push_str(" table.style.display = 'none';\n");
html.push_str(" }\n");
html.push_str(" });\n");
html.push_str(" });\n");
html.push_str(" \n");
html.push_str(" // Zoom functionality\n");
html.push_str(" function zoomIn() {\n");
html.push_str(" currentZoom = Math.min(currentZoom * 1.2, 3);\n");
html.push_str(" applyZoom();\n");
html.push_str(" }\n");
html.push_str(" \n");
html.push_str(" function zoomOut() {\n");
html.push_str(" currentZoom = Math.max(currentZoom / 1.2, 0.5);\n");
html.push_str(" applyZoom();\n");
html.push_str(" }\n");
html.push_str(" \n");
html.push_str(" function resetZoom() {\n");
html.push_str(" currentZoom = 1;\n");
html.push_str(" applyZoom();\n");
html.push_str(" }\n");
html.push_str(" \n");
html.push_str(" function applyZoom() {\n");
html.push_str(" document.body.style.transform = `scale(${currentZoom})`;\n");
html.push_str(" document.body.style.transformOrigin = 'top left';\n");
html.push_str(" }\n");
html.push_str(" \n");
html.push_str(" // Theme toggle\n");
html.push_str(" function toggleTheme() {\n");
html.push_str(" if (currentTheme === 'default') {\n");
html.push_str(" document.body.style.backgroundColor = '#1a1a1a';\n");
html.push_str(" document.body.style.color = '#ffffff';\n");
html.push_str(" currentTheme = 'dark';\n");
html.push_str(" } else {\n");
html.push_str(&format!(
" document.body.style.backgroundColor = '{}';\n",
self.config.theme.background_color
));
html.push_str(&format!(
" document.body.style.color = '{}';\n",
self.config.theme.text_color
));
html.push_str(" currentTheme = 'default';\n");
html.push_str(" }\n");
html.push_str(" }\n");
html.push_str(" \n");
html.push_str(" // Export functions\n");
html.push_str(" function exportSVG() {\n");
html.push_str(" const svg = document.querySelector('.container').outerHTML;\n");
html.push_str(" const blob = new Blob([svg], { type: 'image/svg+xml' });\n");
html.push_str(" const url = URL.createObjectURL(blob);\n");
html.push_str(" const a = document.createElement('a');\n");
html.push_str(" a.href = url;\n");
html.push_str(" a.download = 'schema.svg';\n");
html.push_str(" a.click();\n");
html.push_str(" URL.revokeObjectURL(url);\n");
html.push_str(" }\n");
html.push_str(" \n");
html.push_str(" function exportPDF() {\n");
html.push_str(" window.print();\n");
html.push_str(" }\n");
html.push_str(" \n");
html.push_str(" // Pan functionality\n");
html.push_str(" let isPanning = false;\n");
html.push_str(" let startX, startY, scrollLeft, scrollTop;\n");
html.push_str(" \n");
html.push_str(" document.addEventListener('mousedown', function(e) {\n");
html.push_str(" if (e.button === 1) { // Middle mouse button\n");
html.push_str(" isPanning = true;\n");
html.push_str(" startX = e.pageX;\n");
html.push_str(" startY = e.pageY;\n");
html.push_str(" scrollLeft = window.pageXOffset;\n");
html.push_str(" scrollTop = window.pageYOffset;\n");
html.push_str(" e.preventDefault();\n");
html.push_str(" }\n");
html.push_str(" });\n");
html.push_str(" \n");
html.push_str(" document.addEventListener('mousemove', function(e) {\n");
html.push_str(" if (!isPanning) return;\n");
html.push_str(" e.preventDefault();\n");
html.push_str(" const x = e.pageX;\n");
html.push_str(" const y = e.pageY;\n");
html.push_str(" const walkX = (x - startX) * 2;\n");
html.push_str(" const walkY = (y - startY) * 2;\n");
html.push_str(" window.scrollTo(scrollLeft - walkX, scrollTop - walkY);\n");
html.push_str(" });\n");
html.push_str(" \n");
html.push_str(" document.addEventListener('mouseup', function() {\n");
html.push_str(" isPanning = false;\n");
html.push_str(" });\n");
html.push_str(" \n");
html.push_str(" // Real-time monitoring simulation\n");
html.push_str(" setInterval(function() {\n");
html.push_str(" const timestamp = new Date().toLocaleTimeString();\n");
html.push_str(" document.querySelector('.header p').textContent = `Last updated: ${timestamp}`;\n");
html.push_str(" }, 30000); // Update every 30 seconds\n");
html.push_str(" </script>\n");
html.push_str("</body>\n</html>\n");
Ok(html)
}
/// Generate PlantUML format
pub fn generate_plantuml(&self) -> Result<String, OrmError> {
let mut plantuml = String::new();
plantuml.push_str("@startuml DatabaseSchema\n");
plantuml.push_str("!theme plain\n");
plantuml.push_str("skinparam backgroundColor #FFFFFF\n");
plantuml.push_str("skinparam classBackgroundColor #F8F9FA\n");
plantuml.push_str("skinparam classBorderColor #DEE2E6\n");
plantuml.push_str("skinparam classFontColor #212529\n");
for table in &self.tables {
plantuml.push_str(&format!("class {} {{\n", table.name));
for column in &table.columns {
let mut column_line = format!(" {}", column.name);
if self.config.show_data_types {
column_line.push_str(&format!(": {}", column.data_type));
}
if self.config.show_constraints {
if column.is_primary_key {
column_line.push_str(" <<PK>>");
}
if column.is_foreign_key {
column_line.push_str(" <<FK>>");
}
if !column.is_nullable {
column_line.push_str(" <<NOT NULL>>");
}
}
plantuml.push_str(&format!("{}\n", column_line));
}
plantuml.push_str("}\n");
}
if self.config.show_relationships {
for rel in &self.relationships {
plantuml.push_str(&format!(
"{} ||--o{{ {} : \"{}\"\n",
rel.from_table, rel.to_table, rel.relationship_type
));
}
}
plantuml.push_str("@enduml\n");
Ok(plantuml)
}
/// Generate JSON schema
pub fn generate_json_schema(&self) -> Result<String, OrmError> {
let schema = serde_json::json!({
"metadata": {
"generated_at": chrono::Utc::now().to_rfc3339(),
"total_tables": self.tables.len(),
"total_relationships": self.relationships.len(),
"theme": self.config.theme,
"configuration": {
"show_relationships": self.config.show_relationships,
"show_data_types": self.config.show_data_types,
"show_constraints": self.config.show_constraints,
"layout_engine": self.config.layout_engine
}
},
"tables": self.tables,
"relationships": self.relationships
});
serde_json::to_string_pretty(&schema).map_err(|e| {
OrmError::IoError(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("JSON serialization error: {}", e),
))
})
}
/// Export to SVG format
pub fn export_svg(&self) -> Result<String, OrmError> {
// For now, return the HTML content which can be converted to SVG
// In a full implementation, this would use the `svg` crate to generate proper SVG
self.generate_html()
}
/// Export to PDF format
pub fn export_pdf(&self) -> Result<Vec<u8>, OrmError> {
// This is a placeholder - in a full implementation, this would use the `printpdf` crate
// For now, return an empty PDF structure
let pdf_content = b"%PDF-1.4\n1 0 obj\n<<\n/Type /Catalog\n/Pages 2 0 R\n>>\nendobj\n2 0 obj\n<<\n/Type /Pages\n/Kids [3 0 R]\n/Count 1\n>>\nendobj\n3 0 obj\n<<\n/Type /Page\n/Parent 2 0 R\n/MediaBox [0 0 612 792]\n/Contents 4 0 R\n>>\nendobj\n4 0 obj\n<<\n/Length 0\n>>\nstream\nendstream\nendobj\nxref\n0 5\n0000000000 65535 f \n0000000009 00000 n \n0000000058 00000 n \n0000000115 00000 n \n0000000204 00000 n \ntrailer\n<<\n/Size 5\n/Root 1 0 R\n>>\nstartxref\n297\n%%EOF\n";
Ok(pdf_content.to_vec())
}
/// Export to Visio-compatible format (VSDX)
pub fn export_visio(&self) -> Result<Vec<u8>, OrmError> {
// This is a placeholder - Visio export would require implementing the VSDX format
// For now, return a minimal VSDX structure
let vsdx_content = b"PK\x03\x04\x14\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00[Content_Types].xmlPK\x03\x04\x14\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00_rels/.relsPK\x03\x04\x14\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00visio/document.xmlPK\x03\x04\x14\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00visio/_rels/document.xml.relsPK\x03\x04\x14\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00visio/pages/_rels/page1.xml.relsPK\x03\x04\x14\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00visio/pages/page1.xmlPK\x03\x04\x14\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00visio/windows.xmlPK\x03\x04\x14\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00docProps/app.xmlPK\x03\x04\x14\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00docProps/core.xmlPK\x05\x06\x00\x00\x00\x00\x0b\x00\x0b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";
Ok(vsdx_content.to_vec())
}
}
#[cfg(test)]
mod tests {
use super::*;
use super::{Column, Relationship, Table};
fn create_test_tables() -> Vec<Table> {
vec![
Table {
name: "users".to_string(),
columns: vec![
Column {
name: "id".to_string(),
data_type: "INTEGER".to_string(),
is_nullable: false,
is_primary_key: true,
is_foreign_key: false,
},
Column {
name: "name".to_string(),
data_type: "VARCHAR(255)".to_string(),
is_nullable: false,
is_primary_key: false,
is_foreign_key: false,
},
],
},
Table {
name: "posts".to_string(),
columns: vec![
Column {
name: "id".to_string(),
data_type: "INTEGER".to_string(),
is_nullable: false,
is_primary_key: true,
is_foreign_key: false,
},
Column {
name: "user_id".to_string(),
data_type: "INTEGER".to_string(),
is_nullable: false,
is_primary_key: false,
is_foreign_key: true,
},
Column {
name: "title".to_string(),
data_type: "VARCHAR(255)".to_string(),
is_nullable: false,
is_primary_key: false,
is_foreign_key: false,
},
],
},
]
}
fn create_test_relationships() -> Vec<Relationship> {
vec![Relationship {
from_table: "posts".to_string(),
from_column: "user_id".to_string(),
to_table: "users".to_string(),
to_column: "id".to_string(),
relationship_type: "MANY_TO_ONE".to_string(),
}]
}
#[test]
fn test_generate_dot() {
let visualizer = SchemaVisualizer::new(create_test_tables(), create_test_relationships());
let dot = visualizer.generate_dot().unwrap();
println!("Generated DOT:\n{}", dot);
assert!(dot.contains("digraph DatabaseSchema"));
assert!(dot.contains("users"));
assert!(dot.contains("posts"));
assert!(dot.contains("posts\":\"user_id\" -> \"users\":\"id\""));
}
#[test]
fn test_generate_mermaid() {
let visualizer = SchemaVisualizer::new(create_test_tables(), create_test_relationships());
let mermaid = visualizer.generate_mermaid().unwrap();
assert!(mermaid.contains("erDiagram"));
assert!(mermaid.contains("users {"));
assert!(mermaid.contains("posts {"));
assert!(mermaid.contains("posts ||--o{ users"));
}
#[test]
fn test_generate_html() {
let visualizer = SchemaVisualizer::new(create_test_tables(), create_test_relationships());
let html = visualizer.generate_html().unwrap();
assert!(html.contains("<!DOCTYPE html>"));
assert!(html.contains("users"));
assert!(html.contains("posts"));
assert!(html.contains("Database Schema Visualization"));
}
#[test]
fn test_generate_json_schema() {
let visualizer = SchemaVisualizer::new(create_test_tables(), create_test_relationships());
let json = visualizer.generate_json_schema().unwrap();
assert!(json.contains("users"));
assert!(json.contains("posts"));
assert!(json.contains("total_tables"));
assert!(json.contains("total_relationships"));
}
}