Skip to content

Commit 310128c

Browse files
macmac
authored andcommitted
fix: validate temporal format compatibility
1 parent aeffc7e commit 310128c

1 file changed

Lines changed: 76 additions & 4 deletions

File tree

‎datafusion/common/src/config.rs‎

Lines changed: 76 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2132,13 +2132,48 @@ impl<'a> TryFrom<&'a FormatOptions> for arrow::util::display::FormatOptions<'a>
21322132
("time_format", options.time_format.as_deref()),
21332133
] {
21342134
if let Some(format) = format {
2135-
chrono::format::StrftimeItems::new(format)
2136-
.parse()
2137-
.map_err(|error| {
2135+
let items = chrono::format::StrftimeItems::new(format).parse().map_err(
2136+
|error| {
21382137
DataFusionError::Configuration(format!(
21392138
"Invalid datafusion.format.{name}: {error}"
21402139
))
2141-
})?;
2140+
},
2141+
)?;
2142+
2143+
// Parsing validates directives, but some directives require fields
2144+
// that are absent from a particular temporal type (for example,
2145+
// `%H` for a date). Chrono reports those errors while formatting.
2146+
let mut rendered = String::new();
2147+
let result = match name {
2148+
"date_format" => chrono::NaiveDate::from_ymd_opt(2001, 2, 3)
2149+
.expect("valid sample date")
2150+
.format_with_items(items.iter())
2151+
.write_to(&mut rendered),
2152+
"datetime_format" | "timestamp_format" => {
2153+
chrono::NaiveDate::from_ymd_opt(2001, 2, 3)
2154+
.expect("valid sample date")
2155+
.and_hms_opt(4, 5, 6)
2156+
.expect("valid sample time")
2157+
.format_with_items(items.iter())
2158+
.write_to(&mut rendered)
2159+
}
2160+
"timestamp_tz_format" => {
2161+
chrono::DateTime::from_timestamp(981_173_106, 0)
2162+
.expect("valid sample timestamp")
2163+
.format_with_items(items.iter())
2164+
.write_to(&mut rendered)
2165+
}
2166+
"time_format" => chrono::NaiveTime::from_hms_opt(4, 5, 6)
2167+
.expect("valid sample time")
2168+
.format_with_items(items.iter())
2169+
.write_to(&mut rendered),
2170+
_ => unreachable!("all temporal format names are handled above"),
2171+
};
2172+
if result.is_err() {
2173+
return Err(DataFusionError::Configuration(format!(
2174+
"Invalid datafusion.format.{name}: format is incompatible with its temporal type"
2175+
)));
2176+
}
21422177
}
21432178
}
21442179

@@ -4221,6 +4256,43 @@ mod tests {
42214256
);
42224257
}
42234258

4259+
let incompatible_options = [
4260+
(
4261+
"time_format",
4262+
FormatOptions {
4263+
time_format: Some("%Y".to_owned()),
4264+
..Default::default()
4265+
},
4266+
),
4267+
(
4268+
"date_format",
4269+
FormatOptions {
4270+
date_format: Some("%H".to_owned()),
4271+
..Default::default()
4272+
},
4273+
),
4274+
(
4275+
"timestamp_format",
4276+
FormatOptions {
4277+
timestamp_format: Some("%+".to_owned()),
4278+
..Default::default()
4279+
},
4280+
),
4281+
];
4282+
4283+
for (name, options) in incompatible_options {
4284+
let error = arrow::util::display::FormatOptions::try_from(&options)
4285+
.expect_err(
4286+
"type-incompatible formats must be rejected before rendering",
4287+
);
4288+
assert!(
4289+
error
4290+
.to_string()
4291+
.contains(&format!("datafusion.format.{name}")),
4292+
"unexpected validation error: {error}"
4293+
);
4294+
}
4295+
42244296
arrow::util::display::FormatOptions::try_from(&FormatOptions::default())
42254297
.expect("the default temporal formats must remain valid");
42264298
}

0 commit comments

Comments
 (0)