use rust_xlsxwriter::{Workbook, Worksheet};
use crate::extract::{
extract_column_mappings, extract_script_info, extract_table_dependencies, extract_table_info,
};
use crate::ExportError;
use flowscope_core::AnalyzeResult;
use std::collections::{BTreeSet, HashMap};
pub fn export_xlsx(result: &AnalyzeResult) -> Result<Vec<u8>, ExportError> {
let mut workbook = Workbook::new();
let scripts_sheet = workbook.add_worksheet();
scripts_sheet
.set_name("Scripts")
.map_err(|err| ExportError::Xlsx(err.to_string()))?;
write_scripts_sheet(scripts_sheet, result)?;
let tables_sheet = workbook.add_worksheet();
tables_sheet
.set_name("Tables")
.map_err(|err| ExportError::Xlsx(err.to_string()))?;
write_tables_sheet(tables_sheet, result)?;
let mappings_sheet = workbook.add_worksheet();
mappings_sheet
.set_name("Column Mappings")
.map_err(|err| ExportError::Xlsx(err.to_string()))?;
write_mappings_sheet(mappings_sheet, result)?;
let summary_sheet = workbook.add_worksheet();
summary_sheet
.set_name("Summary")
.map_err(|err| ExportError::Xlsx(err.to_string()))?;
write_summary_sheet(summary_sheet, result)?;
let dependency_sheet = workbook.add_worksheet();
dependency_sheet
.set_name("Dependency Matrix")
.map_err(|err| ExportError::Xlsx(err.to_string()))?;
write_dependency_matrix_sheet(dependency_sheet, result)?;
workbook
.save_to_buffer()
.map_err(|err| ExportError::Xlsx(err.to_string()))
}
fn write_scripts_sheet(sheet: &mut Worksheet, result: &AnalyzeResult) -> Result<(), ExportError> {
let scripts = extract_script_info(result);
write_row(
sheet,
0,
&[
"Script Name",
"Statement Count",
"Tables Read",
"Tables Written",
],
)?;
for (index, script) in scripts.iter().enumerate() {
let row = (index + 1) as u32;
write_row(
sheet,
row,
&[
&sanitize_xlsx_value(&script.source_name),
&script.statement_count.to_string(),
&sanitize_xlsx_value(&script.tables_read.join(", ")),
&sanitize_xlsx_value(&script.tables_written.join(", ")),
],
)?;
}
Ok(())
}
fn write_tables_sheet(sheet: &mut Worksheet, result: &AnalyzeResult) -> Result<(), ExportError> {
let tables = extract_table_info(result);
write_row(
sheet,
0,
&["Table Name", "Qualified Name", "Type", "Columns", "Source"],
)?;
for (index, table) in tables.iter().enumerate() {
let row = (index + 1) as u32;
write_row(
sheet,
row,
&[
&sanitize_xlsx_value(&table.name),
&sanitize_xlsx_value(&table.qualified_name),
table.table_type.as_str(),
&sanitize_xlsx_value(&table.columns.join(", ")),
&sanitize_xlsx_value(table.source_name.as_deref().unwrap_or("")),
],
)?;
}
Ok(())
}
fn write_mappings_sheet(sheet: &mut Worksheet, result: &AnalyzeResult) -> Result<(), ExportError> {
let mappings = extract_column_mappings(result);
write_row(
sheet,
0,
&[
"Source Table",
"Source Column",
"Target Table",
"Target Column",
"Expression",
"Edge Type",
],
)?;
for (index, mapping) in mappings.iter().enumerate() {
let row = (index + 1) as u32;
write_row(
sheet,
row,
&[
&sanitize_xlsx_value(&mapping.source_table),
&sanitize_xlsx_value(&mapping.source_column),
&sanitize_xlsx_value(&mapping.target_table),
&sanitize_xlsx_value(&mapping.target_column),
&sanitize_xlsx_value(mapping.expression.as_deref().unwrap_or("")),
&mapping.edge_type,
],
)?;
}
Ok(())
}
fn write_summary_sheet(sheet: &mut Worksheet, result: &AnalyzeResult) -> Result<(), ExportError> {
let summary = &result.summary;
let metrics = [
("Total Statements", summary.statement_count.to_string()),
("Total Tables", summary.table_count.to_string()),
("Total Columns", summary.column_count.to_string()),
("Total Joins", summary.join_count.to_string()),
("Complexity Score", summary.complexity_score.to_string()),
("Errors", summary.issue_count.errors.to_string()),
("Warnings", summary.issue_count.warnings.to_string()),
("Info", summary.issue_count.infos.to_string()),
];
write_row(sheet, 0, &["Metric", "Value"])?;
for (index, (metric, value)) in metrics.iter().enumerate() {
let row = (index + 1) as u32;
write_row(sheet, row, &[metric, value])?;
}
Ok(())
}
fn write_dependency_matrix_sheet(
sheet: &mut Worksheet,
result: &AnalyzeResult,
) -> Result<(), ExportError> {
let dependencies = extract_table_dependencies(result);
let mut tables = BTreeSet::new();
let mut dep_set = HashMap::new();
for dep in &dependencies {
tables.insert(dep.source_table.clone());
tables.insert(dep.target_table.clone());
dep_set.insert(format!("{}->{}", dep.source_table, dep.target_table), true);
}
let table_list: Vec<String> = tables.into_iter().collect();
let mut header = vec![String::new()];
header.extend(table_list.iter().map(|table| sanitize_xlsx_value(table)));
write_row(
sheet,
0,
&header
.iter()
.map(|value| value.as_str())
.collect::<Vec<_>>(),
)?;
for (row_index, row_table) in table_list.iter().enumerate() {
let mut row = vec![sanitize_xlsx_value(row_table)];
for col_table in &table_list {
let value = if row_table == col_table {
"-".to_string()
} else if dep_set.contains_key(&format!("{}->{}", row_table, col_table)) {
"w".to_string()
} else if dep_set.contains_key(&format!("{}->{}", col_table, row_table)) {
"r".to_string()
} else {
String::new()
};
row.push(value);
}
let row_values: Vec<&str> = row.iter().map(|value| value.as_str()).collect();
write_row(sheet, (row_index + 1) as u32, &row_values)?;
}
let legend_start = (table_list.len() + 2) as u32;
write_row(sheet, legend_start, &["Legend:"])?;
write_row(
sheet,
legend_start + 1,
&["w", "Row table writes to column table"],
)?;
write_row(
sheet,
legend_start + 2,
&["r", "Row table reads from column table"],
)?;
write_row(sheet, legend_start + 3, &["-", "Self (same table)"])?;
Ok(())
}
fn write_row(sheet: &mut Worksheet, row: u32, values: &[&str]) -> Result<(), ExportError> {
for (col, value) in values.iter().enumerate() {
sheet
.write_string(row, col as u16, *value)
.map_err(|err| ExportError::Xlsx(err.to_string()))?;
}
Ok(())
}
fn sanitize_xlsx_value(value: &str) -> String {
if value.is_empty() {
return String::new();
}
let first = value.chars().next().unwrap_or_default();
if first == '=' || first == '+' || first == '-' || first == '@' {
format!("'{}", value)
} else {
value.to_string()
}
}