@@ -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