@@ -22,7 +22,7 @@ use std::cmp::Ordering;
2222use std:: collections:: HashMap ;
2323use std:: fmt:: { Display , Formatter } ;
2424use std:: hash:: Hash ;
25- use std:: sync:: Arc ;
25+ use std:: sync:: { Arc , LazyLock } ;
2626
2727use _serde:: TableMetadataEnum ;
2828use chrono:: { DateTime , Utc } ;
@@ -33,9 +33,9 @@ use uuid::Uuid;
3333use super :: snapshot:: SnapshotReference ;
3434pub use super :: table_metadata_builder:: { TableMetadataBuildResult , TableMetadataBuilder } ;
3535use super :: {
36- DEFAULT_PARTITION_SPEC_ID , PartitionSpecRef , PartitionStatisticsFile , SchemaId , SchemaRef ,
37- SnapshotRef , SnapshotRetention , SortOrder , SortOrderRef , StatisticsFile , StructType ,
38- TableProperties , parse_metadata_file_compression,
36+ DEFAULT_PARTITION_SPEC_ID , PartitionSpecRef , PartitionStatisticsFile , PrimitiveType , Schema ,
37+ SchemaId , SchemaRef , SnapshotRef , SnapshotRetention , SortOrder , SortOrderRef , StatisticsFile ,
38+ StructType , TableProperties , parse_metadata_file_compression,
3939} ;
4040use crate :: catalog:: MetadataLocation ;
4141use crate :: compression:: CompressionCodec ;
@@ -60,6 +60,14 @@ pub const MIN_FORMAT_VERSION_ROW_LINEAGE: FormatVersion = FormatVersion::V3;
6060/// Reference to [`TableMetadata`].
6161pub type TableMetadataRef = Arc < TableMetadata > ;
6262
63+ static PRIMITIVE_TYPE_MIN_FORMAT_VERSION : LazyLock < HashMap < PrimitiveType , FormatVersion > > =
64+ LazyLock :: new ( || {
65+ HashMap :: from ( [
66+ ( PrimitiveType :: TimestampNs , FormatVersion :: V3 ) ,
67+ ( PrimitiveType :: TimestamptzNs , FormatVersion :: V3 ) ,
68+ ] )
69+ } ) ;
70+
6371#[ derive( Debug , PartialEq , Deserialize , Eq , Clone ) ]
6472#[ serde( try_from = "TableMetadataEnum" ) ]
6573/// Fields for the version 2 of the table metadata.
@@ -1565,6 +1573,25 @@ impl Display for FormatVersion {
15651573 }
15661574}
15671575
1576+ /// Returns the minimum table format version required by any type in a schema.
1577+ ///
1578+ /// Returns [`None`] when the schema contains no type with a specific minimum
1579+ /// table format version requirement.
1580+ pub fn min_format_version_for_schema ( schema : & Schema ) -> Option < FormatVersion > {
1581+ schema
1582+ . field_id_to_fields ( )
1583+ . values ( )
1584+ . filter_map ( |field| field. field_type . as_primitive_type ( ) )
1585+ . filter_map ( min_format_version_for_primitive_type)
1586+ . max ( )
1587+ }
1588+
1589+ pub ( crate ) fn min_format_version_for_primitive_type (
1590+ primitive : & PrimitiveType ,
1591+ ) -> Option < FormatVersion > {
1592+ PRIMITIVE_TYPE_MIN_FORMAT_VERSION . get ( primitive) . copied ( )
1593+ }
1594+
15681595#[ derive( Debug , Serialize , Deserialize , PartialEq , Eq , Clone ) ]
15691596#[ serde( rename_all = "kebab-case" ) ]
15701597/// Encodes changes to the previous metadata files for the table
@@ -1616,10 +1643,10 @@ mod tests {
16161643 use crate :: io:: FileIO ;
16171644 use crate :: spec:: table_metadata:: TableMetadata ;
16181645 use crate :: spec:: {
1619- BlobMetadata , EncryptedKey , INITIAL_ROW_ID , Literal , NestedField , NullOrder , Operation ,
1620- PartitionSpec , PartitionStatisticsFile , PrimitiveLiteral , PrimitiveType , Schema , Snapshot ,
1621- SnapshotReference , SnapshotRetention , SortDirection , SortField , SortOrder , StatisticsFile ,
1622- Summary , TableProperties , Transform , Type , UnboundPartitionField ,
1646+ BlobMetadata , EncryptedKey , INITIAL_ROW_ID , ListType , Literal , NestedField , NullOrder ,
1647+ Operation , PartitionSpec , PartitionStatisticsFile , PrimitiveLiteral , PrimitiveType , Schema ,
1648+ Snapshot , SnapshotReference , SnapshotRetention , SortDirection , SortField , SortOrder ,
1649+ StatisticsFile , Summary , TableProperties , Transform , Type , UnboundPartitionField ,
16231650 } ;
16241651 use crate :: { ErrorKind , TableCreation } ;
16251652
@@ -3541,6 +3568,28 @@ mod tests {
35413568 )
35423569 }
35433570
3571+ fn schema_with_primitive_field ( field_type : PrimitiveType ) -> Schema {
3572+ Schema :: builder ( )
3573+ . with_fields ( vec ! [
3574+ NestedField :: required( 1 , "ts" , Type :: Primitive ( field_type) ) . into( ) ,
3575+ ] )
3576+ . build ( )
3577+ . unwrap ( )
3578+ }
3579+
3580+ fn table_creation_with_format_version (
3581+ schema : Schema ,
3582+ format_version : FormatVersion ,
3583+ ) -> TableCreation {
3584+ TableCreation :: builder ( )
3585+ . location ( "s3://db/table" . to_string ( ) )
3586+ . name ( "table" . to_string ( ) )
3587+ . properties ( HashMap :: new ( ) )
3588+ . schema ( schema)
3589+ . format_version ( format_version)
3590+ . build ( )
3591+ }
3592+
35443593 #[ test]
35453594 fn test_table_metadata_builder_from_table_creation ( ) {
35463595 let table_creation = TableCreation :: builder ( )
@@ -3591,6 +3640,91 @@ mod tests {
35913640 ) ;
35923641 }
35933642
3643+ #[ test]
3644+ fn test_table_metadata_builder_rejects_v1_v2_nanosecond_timestamp_tables ( ) {
3645+ for ( format_version, primitive_type) in [
3646+ ( FormatVersion :: V1 , PrimitiveType :: TimestampNs ) ,
3647+ ( FormatVersion :: V1 , PrimitiveType :: TimestamptzNs ) ,
3648+ ( FormatVersion :: V2 , PrimitiveType :: TimestampNs ) ,
3649+ ( FormatVersion :: V2 , PrimitiveType :: TimestamptzNs ) ,
3650+ ] {
3651+ let table_creation = table_creation_with_format_version (
3652+ schema_with_primitive_field ( primitive_type) ,
3653+ format_version,
3654+ ) ;
3655+
3656+ let err = TableMetadataBuilder :: from_table_creation ( table_creation) . unwrap_err ( ) ;
3657+
3658+ assert_eq ! ( err. kind( ) , ErrorKind :: DataInvalid ) ;
3659+ assert ! (
3660+ err. message( ) . contains( "Invalid type for ts:" ) ,
3661+ "expected error message to name the invalid column, got {}" ,
3662+ err. message( )
3663+ ) ;
3664+ assert ! (
3665+ err. message( ) . contains( "is not supported until v3" ) ,
3666+ "expected error message to explain v3 requirement, got {}" ,
3667+ err. message( )
3668+ ) ;
3669+ }
3670+ }
3671+
3672+ #[ test]
3673+ fn test_table_metadata_builder_rejects_v2_list_element_requiring_v3 ( ) {
3674+ let schema = Schema :: builder ( )
3675+ . with_fields ( vec ! [
3676+ NestedField :: required(
3677+ 1 ,
3678+ "ts_values" ,
3679+ Type :: List ( ListType :: new(
3680+ NestedField :: list_element(
3681+ 2 ,
3682+ Type :: Primitive ( PrimitiveType :: TimestampNs ) ,
3683+ false ,
3684+ )
3685+ . into( ) ,
3686+ ) ) ,
3687+ )
3688+ . into( ) ,
3689+ ] )
3690+ . build ( )
3691+ . unwrap ( ) ;
3692+ let table_creation = table_creation_with_format_version ( schema, FormatVersion :: V2 ) ;
3693+
3694+ let err = TableMetadataBuilder :: from_table_creation ( table_creation) . unwrap_err ( ) ;
3695+
3696+ assert_eq ! ( err. kind( ) , ErrorKind :: DataInvalid ) ;
3697+ assert ! (
3698+ err. message( ) . contains(
3699+ "Invalid type for ts_values.element: timestamp_ns is not supported until v3"
3700+ ) ,
3701+ "expected error message to explain nested v3 requirement with column name, got {}" ,
3702+ err. message( )
3703+ ) ;
3704+ }
3705+
3706+ #[ test]
3707+ fn test_table_metadata_builder_allows_v3_nanosecond_timestamp_tables ( ) {
3708+ let schema = Schema :: builder ( )
3709+ . with_fields ( vec ! [
3710+ NestedField :: required( 1 , "ts_ns" , Type :: Primitive ( PrimitiveType :: TimestampNs ) )
3711+ . into( ) ,
3712+ NestedField :: required( 2 , "tstz_ns" , Type :: Primitive ( PrimitiveType :: TimestamptzNs ) )
3713+ . into( ) ,
3714+ ] )
3715+ . build ( )
3716+ . unwrap ( ) ;
3717+ let table_creation = table_creation_with_format_version ( schema, FormatVersion :: V3 ) ;
3718+
3719+ let table_metadata = TableMetadataBuilder :: from_table_creation ( table_creation)
3720+ . unwrap ( )
3721+ . build ( )
3722+ . unwrap ( )
3723+ . metadata ;
3724+
3725+ assert_eq ! ( table_metadata. format_version, FormatVersion :: V3 ) ;
3726+ }
3727+
35943728 #[ tokio:: test]
35953729 async fn test_table_metadata_read_write ( ) {
35963730 // Create a temporary directory for our test
0 commit comments