diff --git a/client-gui2 b/client-gui2 index 0a536189..0461f0f6 160000 --- a/client-gui2 +++ b/client-gui2 @@ -1 +1 @@ -Subproject commit 0a536189bbc017f9174cbc5d276828a84c16f2e0 +Subproject commit 0461f0f6021109bd0aa97426686b41f835a83a9d diff --git a/common/src/proto/descriptor.bin b/common/src/proto/descriptor.bin index adaa6b43..bd7a59ab 100644 Binary files a/common/src/proto/descriptor.bin and b/common/src/proto/descriptor.bin differ diff --git a/server b/server index 66ce2877..41a34de4 160000 --- a/server +++ b/server @@ -1 +1 @@ -Subproject commit 66ce2877296a032349140ad524c555edca457cd0 +Subproject commit 41a34de46f7c206e80f723167fef70789638c62a diff --git a/web/src/pages/import_export/import/logic.rs b/web/src/pages/import_export/import/logic.rs index b652b5b1..c122573b 100644 --- a/web/src/pages/import_export/import/logic.rs +++ b/web/src/pages/import_export/import/logic.rs @@ -74,6 +74,20 @@ impl Destination { .map(|column| column.name.clone()) .collect() } + + fn is_verification(&self, name: &str) -> bool { + self.columns + .iter() + .any(|column| column.name == name && column.verification) + } + + fn writable_types(&self) -> HashMap { + self.types + .iter() + .filter(|(name, _)| !self.is_verification(name)) + .map(|(name, data_type)| (name.clone(), data_type.clone())) + .collect() + } } /// GET /admin/import @@ -625,7 +639,8 @@ async fn prepared( read_mapping(locale, &chosen, &source).map_err(|message| reject(headers, message))?; let prepared_names = destination.prepared_names(&assignments); let mut prepared = prepare(&assignments, &source, &prepared_names); - normalize_dates(locale, &mut prepared, &destination.types, form.date_format) + let writable_types = destination.writable_types(); + normalize_dates(locale, &mut prepared, &writable_types, form.date_format) .map_err(|message| reject(headers, message))?; Ok((destination, source, prepared)) } @@ -657,7 +672,8 @@ fn source_options(locale: Locale, source: &Source) -> Vec { } /// One prepared row as the staged import takes it: values under their destination -/// names, converted to the column's type. +/// names. Writable values are converted to their column type; projection +/// assertions retain the CSV cell's exact text. fn import_row( locale: Locale, destination: &Destination, @@ -666,6 +682,21 @@ fn import_row( ) -> Result { let mut data = HashMap::new(); for (index, column) in columns.iter().enumerate() { + let raw = row.get(index).map(String::as_str).unwrap_or_default(); + if destination.is_verification(column) { + let kind = if raw.is_empty() { + prost_types::value::Kind::NullValue(prost_types::NullValue::NullValue as i32) + } else { + prost_types::value::Kind::StringValue(raw.to_string()) + }; + data.insert( + column.clone(), + prost_types::Value { + kind: Some(kind), + }, + ); + continue; + } let data_type = destination.types.get(column).ok_or_else(|| { tr!( locale, @@ -673,11 +704,7 @@ fn import_row( "column" => column.clone(), ) })?; - let value = csv_value( - locale, - row.get(index).map(String::as_str).unwrap_or_default(), - data_type, - )?; + let value = csv_value(locale, raw, data_type)?; data.insert(column.clone(), value); } Ok(TableDataImportRow { data, link_display_columns: Vec::new() }) @@ -773,6 +800,9 @@ fn unavailable(headers: &HeaderMap, message: String) -> Response { #[cfg(test)] mod tests { use super::*; + use prost_types::value::Kind; + + use crate::pages::import_export::import::destination::DestinationKey; fn source() -> Source { read_source( @@ -801,4 +831,48 @@ mod tests { assert!(options[3].label.contains('4'), "{}", options[3].label); } + + #[test] + fn projection_assertions_keep_exact_csv_text() { + let destination = Destination { + table_name: "orders".to_string(), + table_revision: 1, + columns: vec![ + DestinationColumn { + key: DestinationKey::Column(1), + name: "projected_amount".to_string(), + required: false, + verification: true, + }, + DestinationColumn { + key: DestinationKey::Column(2), + name: "ordinary_number".to_string(), + required: false, + verification: false, + }, + ], + types: HashMap::from([ + ("projected_amount".to_string(), "NUMERIC".to_string()), + ("ordinary_number".to_string(), "BIGINT".to_string()), + ]), + }; + + let row = import_row( + Locale::default(), + &destination, + &["projected_amount".to_string(), "ordinary_number".to_string()], + &["10.00".to_string(), "001".to_string()], + ) + .unwrap(); + + assert_eq!( + row.data["projected_amount"].kind, + Some(Kind::StringValue("10.00".to_string())) + ); + assert_eq!( + row.data["ordinary_number"].kind, + Some(Kind::NumberValue(1.0)) + ); + assert!(!destination.writable_types().contains_key("projected_amount")); + } }