use anyhow::Result;
use crate::cli::{ConvCommand, HeaderCase, JsonMode, XmlMode};
use crate::input_spec::parse_input_specs;
use crate::output::{parse_output_path_spec, write_query_result};
pub(crate) fn run_conv_command(cmd: ConvCommand) -> Result<()> {
let result = execute_conv_command(&cmd)?;
let (output_path, output_format_hint) = parse_output_path_spec(cmd.output.as_deref());
write_query_result(
&result,
output_path.as_deref(),
cmd.format,
output_format_hint.as_deref(),
)?;
Ok(())
}
pub(crate) fn execute_conv_command(cmd: &ConvCommand) -> Result<query_forge::QueryResult> {
let type_inference_options = resolve_type_inference_options(&cmd);
let input_normalization_options = resolve_input_normalization_options(&cmd);
let extraction_options = resolve_extraction_options(&cmd);
let input_specs = parse_input_specs(&cmd.input, None)?;
let workbook_inputs = input_specs
.iter()
.map(|input| query_forge::WorkbookInput {
path: input.path.as_path(),
sheet_name: input.sheet_name.as_deref(),
table_name: input.table_name.as_deref(),
explicit_format: input.explicit_format.as_deref(),
})
.collect::<Vec<_>>();
let sql = build_select_all_sql(&workbook_inputs);
query_forge::run_query_with_params_multi_inputs_and_options_and_normalization(
&workbook_inputs,
&sql,
&[],
&type_inference_options,
&input_normalization_options,
&extraction_options,
!cmd.no_headers,
)
}
fn build_select_all_sql(inputs: &[query_forge::WorkbookInput<'_>]) -> String {
let table_names: Vec<String> = inputs
.iter()
.enumerate()
.map(|(i, input)| {
if let Some(name) = input.table_name {
name.to_owned()
} else if i == 0 {
"table".to_owned()
} else {
format!("table{}", i + 1)
}
})
.collect();
if table_names.len() == 1 {
format!("SELECT * FROM \"{}\"", table_names[0])
} else {
table_names
.iter()
.map(|name| format!("SELECT * FROM \"{name}\""))
.collect::<Vec<_>>()
.join(" UNION ALL ")
}
}
fn resolve_type_inference_options(cmd: &ConvCommand) -> query_forge::TypeInferenceOptions {
let defaults = query_forge::TypeInferenceOptions::default();
query_forge::TypeInferenceOptions {
infer_types: !cmd.all_text || cmd.infer_types,
decimal_comma: cmd.decimal_comma,
date_format: cmd.date_format.clone(),
null_values: if cmd.null_values.is_empty() {
defaults.null_values
} else {
cmd.null_values
.iter()
.map(|value| value.trim().to_owned())
.collect()
},
true_values: if cmd.true_values.is_empty() {
defaults.true_values
} else {
cmd.true_values
.iter()
.map(|value| value.trim().to_owned())
.collect()
},
false_values: if cmd.false_values.is_empty() {
defaults.false_values
} else {
cmd.false_values
.iter()
.map(|value| value.trim().to_owned())
.collect()
},
}
}
fn resolve_input_normalization_options(
cmd: &ConvCommand,
) -> query_forge::InputNormalizationOptions {
query_forge::InputNormalizationOptions {
trim: cmd.trim,
skip_empty_rows: cmd.skip_empty_rows,
normalize_headers: cmd.normalize_headers,
header_case: match cmd.header_case {
Some(HeaderCase::Snake) => Some(query_forge::HeaderCase::Snake),
Some(HeaderCase::Camel) => Some(query_forge::HeaderCase::Camel),
Some(HeaderCase::Pascal) => Some(query_forge::HeaderCase::Pascal),
Some(HeaderCase::ScreamingSnake) => Some(query_forge::HeaderCase::ScreamingSnake),
None => None,
},
dedupe_headers: cmd.dedupe_headers,
}
}
fn resolve_extraction_options(cmd: &ConvCommand) -> query_forge::ExtractionOptions {
query_forge::ExtractionOptions {
json_mode: match cmd.json_mode {
Some(JsonMode::Array) | None => query_forge::JsonMode::Array,
Some(JsonMode::Object) => query_forge::JsonMode::Object,
Some(JsonMode::Flatten) => query_forge::JsonMode::Flatten,
},
xml_mode: match cmd.xml_mode {
Some(XmlMode::Rows) | None => query_forge::XmlMode::Rows,
Some(XmlMode::Descendants) => query_forge::XmlMode::Descendants,
Some(XmlMode::Attributes) => query_forge::XmlMode::Attributes,
},
}
}
#[cfg(test)]
mod tests {
use super::build_select_all_sql;
fn make_input<'a>(
table_name: Option<&'a str>,
) -> query_forge::WorkbookInput<'a> {
query_forge::WorkbookInput {
path: std::path::Path::new("dummy.csv"),
sheet_name: None,
table_name,
explicit_format: None,
}
}
#[test]
fn single_input_no_name_becomes_table() {
let inputs = vec![make_input(None)];
assert_eq!(build_select_all_sql(&inputs), "SELECT * FROM \"table\"");
}
#[test]
fn single_input_with_explicit_name() {
let inputs = vec![make_input(Some("sales"))];
assert_eq!(build_select_all_sql(&inputs), "SELECT * FROM \"sales\"");
}
#[test]
fn two_inputs_unnamed_produce_union_all() {
let inputs = vec![make_input(None), make_input(None)];
assert_eq!(
build_select_all_sql(&inputs),
"SELECT * FROM \"table\" UNION ALL SELECT * FROM \"table2\""
);
}
#[test]
fn three_inputs_unnamed() {
let inputs = vec![make_input(None), make_input(None), make_input(None)];
assert_eq!(
build_select_all_sql(&inputs),
"SELECT * FROM \"table\" UNION ALL SELECT * FROM \"table2\" UNION ALL SELECT * FROM \"table3\""
);
}
#[test]
fn two_inputs_with_explicit_names() {
let inputs = vec![make_input(Some("a")), make_input(Some("b"))];
assert_eq!(
build_select_all_sql(&inputs),
"SELECT * FROM \"a\" UNION ALL SELECT * FROM \"b\""
);
}
}