parent
a1918ec8c2
commit
0c9b6c7e1d
|
|
@ -15,6 +15,7 @@ import { useToast } from "@/composables/useToast";
|
|||
import {
|
||||
autoMapImportColumns,
|
||||
buildTableImportParseOptions,
|
||||
defaultTableImportEmptyStringAsNull,
|
||||
formatTableImportElapsed,
|
||||
nextTableImportWizardStep,
|
||||
previousTableImportWizardStep,
|
||||
|
|
@ -90,7 +91,7 @@ const titleRow = ref(1);
|
|||
const dataStartRow = ref(2);
|
||||
const lastDataRow = ref(0);
|
||||
const trimValues = ref(false);
|
||||
const emptyStringAsNull = ref(true);
|
||||
const emptyStringAsNull = ref(defaultTableImportEmptyStringAsNull(sourceFormat.value));
|
||||
const selectedSheet = ref("");
|
||||
const jsonShape = ref<api.TableImportJsonShape>("auto");
|
||||
const previewLimit = ref(50);
|
||||
|
|
@ -250,7 +251,7 @@ function resetState() {
|
|||
dataStartRow.value = 2;
|
||||
lastDataRow.value = 0;
|
||||
trimValues.value = false;
|
||||
emptyStringAsNull.value = true;
|
||||
emptyStringAsNull.value = defaultTableImportEmptyStringAsNull(sourceFormat.value);
|
||||
selectedSheet.value = "";
|
||||
jsonShape.value = "auto";
|
||||
previewLimit.value = 50;
|
||||
|
|
@ -519,6 +520,7 @@ function assignSelectedSource(source: string | File) {
|
|||
errorMessage.value = "";
|
||||
const name = typeof source === "string" ? source : source.name;
|
||||
sourceFormat.value = detectFormat(name);
|
||||
emptyStringAsNull.value = defaultTableImportEmptyStringAsNull(sourceFormat.value);
|
||||
if (!newTableName.value.trim()) {
|
||||
newTableName.value = suggestedTableName(name);
|
||||
}
|
||||
|
|
@ -554,9 +556,11 @@ async function prepareBatchSources(sources: ImportSource[]) {
|
|||
errorMessage.value = "";
|
||||
const tasks: BatchImportTask[] = [];
|
||||
const usedNames = new Set<string>();
|
||||
const formats = sources.map((source) => detectFormat(sourceName(source)));
|
||||
emptyStringAsNull.value = formats.length && formats.every((format) => format === formats[0]) ? defaultTableImportEmptyStringAsNull(formats[0]!) : true;
|
||||
try {
|
||||
for (const source of sources) {
|
||||
const format = detectFormat(sourceName(source));
|
||||
for (const [index, source] of sources.entries()) {
|
||||
const format = formats[index]!;
|
||||
const initialPreview = await api.previewTableImportFile(source, {
|
||||
sourceFormat: format,
|
||||
parseOptions: taskParseOptions(format),
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
|||
import {
|
||||
autoMapImportColumns,
|
||||
buildTableImportParseOptions,
|
||||
defaultTableImportEmptyStringAsNull,
|
||||
formatTableImportElapsed,
|
||||
nextTableImportWizardStep,
|
||||
previousTableImportWizardStep,
|
||||
|
|
@ -13,6 +14,14 @@ import {
|
|||
} from "@/lib/table/tableImport";
|
||||
|
||||
describe("tableImport", () => {
|
||||
it("preserves explicit empty strings by default for Excel only", () => {
|
||||
expect(defaultTableImportEmptyStringAsNull("excel")).toBe(false);
|
||||
expect(defaultTableImportEmptyStringAsNull("csv")).toBe(true);
|
||||
expect(defaultTableImportEmptyStringAsNull("tsv")).toBe(true);
|
||||
expect(defaultTableImportEmptyStringAsNull("delimited")).toBe(true);
|
||||
expect(defaultTableImportEmptyStringAsNull("json")).toBe(true);
|
||||
});
|
||||
|
||||
it("formats import elapsed time for progress and terminal summaries", () => {
|
||||
expect(formatTableImportElapsed(0)).toBe("0 ms");
|
||||
expect(formatTableImportElapsed(999)).toBe("999 ms");
|
||||
|
|
|
|||
|
|
@ -88,6 +88,10 @@ export interface TableImportParseSettings {
|
|||
jsonShape: TableImportJsonShape;
|
||||
}
|
||||
|
||||
export function defaultTableImportEmptyStringAsNull(format: TableImportSourceFormat): boolean {
|
||||
return format !== "excel";
|
||||
}
|
||||
|
||||
export function buildTableImportParseOptions(settings: TableImportParseSettings): TableImportParseOptions {
|
||||
const isDelimited = settings.format === "csv" || settings.format === "tsv" || settings.format === "delimited";
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -6719,6 +6719,42 @@ mod tests {
|
|||
assert_xlsx_empty_string_option(TableImportParseOptions::default(), vec![serde_json::Value::Null; 5]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dbx_exported_xlsx_round_trip_preserves_empty_strings_when_configured() {
|
||||
let path =
|
||||
std::env::temp_dir().join(format!("dbx-table-import-empty-round-trip-{}.xlsx", uuid::Uuid::new_v4()));
|
||||
let workbook = build_xlsx_workbook_multi(&[XlsxWorksheetData {
|
||||
sheet_name: Some("Data".to_string()),
|
||||
columns: vec!["empty_text".to_string(), "missing_value".to_string()],
|
||||
column_types: vec!["VARCHAR(255)".to_string(), "VARCHAR(255)".to_string()],
|
||||
column_comments: vec![],
|
||||
rows: vec![vec![serde_json::json!(""), serde_json::Value::Null]],
|
||||
numeric_column_right_align: false,
|
||||
}])
|
||||
.unwrap();
|
||||
std::fs::write(&path, workbook).unwrap();
|
||||
let options =
|
||||
TableImportParseOptions { empty_string_as_null: Some(false), ..TableImportParseOptions::default() };
|
||||
|
||||
let parsed = parse_xlsx_file_with_options(&path.to_string_lossy(), &options, 10).unwrap();
|
||||
let (preview, _) = parse_xlsx_preview_file_with_options(&path.to_string_lossy(), &options, 10).unwrap();
|
||||
let (sender, mut receiver) = tokio::sync::mpsc::channel(16);
|
||||
stream_xlsx_rows_to_channel(&path.to_string_lossy(), &options, 500, None, HashSet::new(), false, sender)
|
||||
.unwrap();
|
||||
let mut streamed_rows = Vec::new();
|
||||
while let Some(message) = receiver.blocking_recv() {
|
||||
if let XlsxStreamMessage::Rows(rows) = message.unwrap() {
|
||||
streamed_rows.extend(rows);
|
||||
}
|
||||
}
|
||||
|
||||
let expected = vec![vec![serde_json::json!(""), serde_json::Value::Null]];
|
||||
assert_eq!(parsed.rows, expected);
|
||||
assert_eq!(preview.rows, parsed.rows);
|
||||
assert_eq!(streamed_rows, parsed.rows);
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retains_only_temporal_and_text_target_xlsx_styles() {
|
||||
let styles = vec![
|
||||
|
|
|
|||
Loading…
Reference in New Issue