1 parent
521fa2eb83
commit
5981c20675
4 files changed
+130
-7
No files matched your search
@@ -646,13 +646,13 @@ fn safe_excel_number(value: &str) -> Option<&str> {
|
||||
if trimmed.is_empty() || trimmed.parse::<f64>().ok().is_none_or(|number| !number.is_finite()) {
|
||||
return None;
|
||||
}
|
||||
// Excel stores numbers with at most 15 significant decimal digits. Keep
|
||||
// higher-precision database values as text rather than silently rounding.
|
||||
let significant_digits = trimmed
|
||||
.split(['e', 'E'])
|
||||
.next()
|
||||
.unwrap_or(trimmed)
|
||||
// Fractional trailing zeros preserve database scale but add no numeric
|
||||
// precision. Keep values with more than 15 actual digits as text.
|
||||
let significand = trimmed.split(['e', 'E']).next().unwrap_or(trimmed);
|
||||
let (integer, fraction) = significand.split_once('.').unwrap_or((significand, ""));
|
||||
let significant_digits = integer
|
||||
.chars()
|
||||
.chain(fraction.trim_end_matches('0').chars())
|
||||
.filter(|ch| ch.is_ascii_digit())
|
||||
.skip_while(|ch| *ch == '0')
|
||||
.count();
|
||||
@@ -1141,6 +1141,41 @@ mod tests {
|
||||
assert!(sheet.contains("<c r=\"C2\" t=\"inlineStr\"><is><t>00123</t></is></c>"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_fractional_trailing_zeros_when_checking_excel_numeric_precision() {
|
||||
let workbook = build_xlsx_workbook(&XlsxWorksheetData {
|
||||
sheet_name: Some("Numeric precision".to_string()),
|
||||
columns: vec![
|
||||
"reported".to_string(),
|
||||
"negative_boundary".to_string(),
|
||||
"safe_boundary".to_string(),
|
||||
"unsafe_integer".to_string(),
|
||||
"precise_fraction".to_string(),
|
||||
"fallback".to_string(),
|
||||
],
|
||||
column_types: vec!["numeric".to_string(); 6],
|
||||
column_comments: vec![],
|
||||
rows: vec![vec![
|
||||
json!("-100000.0000000000"),
|
||||
json!("-999999999999999.0000"),
|
||||
json!("123456789012345.0000000000"),
|
||||
json!("1234567890123456.0000"),
|
||||
json!("100000.0000000001"),
|
||||
json!("not-a-number"),
|
||||
]],
|
||||
numeric_column_right_align: true,
|
||||
})
|
||||
.expect("build workbook");
|
||||
|
||||
let sheet = read_zip_entry(&workbook, "xl/worksheets/sheet1.xml");
|
||||
assert!(sheet.contains("<c r=\"A2\" s=\"4\"><v>-100000.0000000000</v></c>"));
|
||||
assert!(sheet.contains("<c r=\"B2\" s=\"4\"><v>-999999999999999.0000</v></c>"));
|
||||
assert!(sheet.contains("<c r=\"C2\" s=\"4\"><v>123456789012345.0000000000</v></c>"));
|
||||
assert!(sheet.contains("<c r=\"D2\" t=\"inlineStr\" s=\"4\"><is><t>1234567890123456.0000</t></is></c>"));
|
||||
assert!(sheet.contains("<c r=\"E2\" t=\"inlineStr\" s=\"4\"><is><t>100000.0000000001</t></is></c>"));
|
||||
assert!(sheet.contains("<c r=\"F2\" t=\"inlineStr\" s=\"4\"><is><t>not-a-number</t></is></c>"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn writes_temporal_columns_as_excel_dates_without_retyping_other_values() {
|
||||
let workbook = build_xlsx_workbook(&XlsxWorksheetData {
|
||||
|
||||
@@ -241,6 +241,76 @@ async fn live_postgres_query_result_xlsx_preserves_temporal_cell_types() {
|
||||
cleanup_result.expect("cleanup temporal export fixture");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires DBX_LIVE_POSTGRES_HOST/PORT/USER/PASSWORD/DATABASE pointing at PostgreSQL 16.3"]
|
||||
async fn live_postgres_numeric_xlsx_ignores_fractional_trailing_zeros() {
|
||||
let host = std::env::var("DBX_LIVE_POSTGRES_HOST").unwrap_or_else(|_| "127.0.0.1".to_string());
|
||||
let port = std::env::var("DBX_LIVE_POSTGRES_PORT").ok().and_then(|value| value.parse().ok()).unwrap_or(5432);
|
||||
let user = std::env::var("DBX_LIVE_POSTGRES_USER").unwrap_or_else(|_| "postgres".to_string());
|
||||
let password = std::env::var("DBX_LIVE_POSTGRES_PASSWORD").unwrap_or_default();
|
||||
let database = std::env::var("DBX_LIVE_POSTGRES_DATABASE").unwrap_or_else(|_| "postgres".to_string());
|
||||
|
||||
let suffix = uuid::Uuid::new_v4().simple().to_string();
|
||||
let dir = std::env::temp_dir().join(format!("dbx-live-postgres-xlsx-numeric-{suffix}"));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let storage = Storage::open(&dir.join("storage.db")).await.unwrap();
|
||||
let state = AppState::new(storage);
|
||||
let connection_id = "live-postgres-xlsx-numeric";
|
||||
let config = live_postgres_config(connection_id, &host, port, &user, &password, &database);
|
||||
state.configs.write().await.insert(config.id.clone(), config);
|
||||
|
||||
let file_path = dir.join("result.xlsx");
|
||||
let sql = "SELECT fee FROM (VALUES \
|
||||
(1, (-100000.0000000000)::numeric), \
|
||||
(2, (-999999999999999.0000)::numeric), \
|
||||
(3, (123456789012345.0000000000)::numeric), \
|
||||
(4, (1234567890123456.0000)::numeric), \
|
||||
(5, (100000.0000000001)::numeric)) AS v(ord, fee) ORDER BY ord"
|
||||
.to_string();
|
||||
let request = QueryResultExportRequest {
|
||||
export_id: format!("live-postgres-xlsx-numeric-{suffix}"),
|
||||
connection_id: connection_id.to_string(),
|
||||
database: database.clone(),
|
||||
schema: None,
|
||||
catalog: None,
|
||||
sql: sql.clone(),
|
||||
query_base_sql: sql,
|
||||
setup_sql: Vec::new(),
|
||||
database_type: DatabaseType::Postgres,
|
||||
use_agent_cursor: false,
|
||||
file_path: file_path.to_string_lossy().to_string(),
|
||||
format: "xlsx".to_string(),
|
||||
include_sql_sheet: false,
|
||||
page_size: 100,
|
||||
row_limit: None,
|
||||
total_rows: None,
|
||||
timeout_secs: Some(30),
|
||||
keyset_optimization_enabled: false,
|
||||
client_session_id: Some(format!("live-postgres-xlsx-numeric-{suffix}")),
|
||||
execution_id: Some(format!("live-postgres-xlsx-numeric-{suffix}")),
|
||||
date_time_format: None,
|
||||
export_table_name: None,
|
||||
export_column_types: None,
|
||||
column_comments: None,
|
||||
numeric_column_right_align: true,
|
||||
};
|
||||
|
||||
export_query_result_core(&state, &request, None, |_| {}).await.expect("export numeric XLSX");
|
||||
|
||||
let workbook = std::fs::read(&file_path).unwrap();
|
||||
let mut archive = zip::ZipArchive::new(std::io::Cursor::new(workbook)).expect("open generated XLSX");
|
||||
let mut sheet = String::new();
|
||||
archive.by_name("xl/worksheets/sheet1.xml").unwrap().read_to_string(&mut sheet).unwrap();
|
||||
assert!(sheet.contains("<c r=\"A2\" s=\"4\"><v>-100000.0000000000</v></c>"), "sheet={sheet}");
|
||||
assert!(sheet.contains("<c r=\"A3\" s=\"4\"><v>-999999999999999.0000</v></c>"), "sheet={sheet}");
|
||||
assert!(sheet.contains("<c r=\"A4\" s=\"4\"><v>123456789012345.0000000000</v></c>"), "sheet={sheet}");
|
||||
assert!(sheet.contains("<c r=\"A5\" t=\"inlineStr\" s=\"4\"><is><t>1234567890123456.0000</t></is></c>"));
|
||||
assert!(sheet.contains("<c r=\"A6\" t=\"inlineStr\" s=\"4\"><is><t>100000.0000000001</t></is></c>"));
|
||||
|
||||
drop(archive);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires DBX_LIVE_POSTGRES_* env vars for temporary-table CSV/XLSX export"]
|
||||
async fn live_postgres_truncated_batch_result_export_replays_safe_temp_setup() {
|
||||
|
||||
Reference in new issue
Block a user