@@ -568,6 +568,189 @@ impl TableConfiguration {
568568 }
569569}
570570
571+ /// Configuration parsed from user-provided table properties during table creation.
572+ ///
573+ /// This struct extracts signal flags (protocol versions, feature overrides) from
574+ /// the input properties and separates them from the table properties that will
575+ /// be stored in metadata.
576+ ///
577+ /// Signal flags include:
578+ /// - `delta.feature.<name> = supported` - Explicit feature enablement
579+ /// - `delta.minReaderVersion` - Protocol reader version hint
580+ /// - `delta.minWriterVersion` - Protocol writer version hint
581+ ///
582+ /// These signal flags affect protocol creation but are not stored in the table's
583+ /// metadata configuration.
584+ ///
585+ /// # Example
586+ /// ```ignore
587+ /// let props = HashMap::from([
588+ /// ("delta.feature.deletionVectors".to_string(), "supported".to_string()),
589+ /// ("delta.minReaderVersion".to_string(), "3".to_string()),
590+ /// ("myapp.version".to_string(), "1.0".to_string()),
591+ /// ]);
592+ /// let config = TableCreationConfig::try_from(props)?;
593+ /// config.validate_for_create()?;
594+ /// // config.reader_features = [TableFeature::DeletionVectors]
595+ /// // config.writer_features = [TableFeature::DeletionVectors]
596+ /// // config.min_reader_version = Some(3)
597+ /// // config.table_properties = {"myapp.version": "1.0"}
598+ /// ```
599+ #[ derive( Debug , Default ) ]
600+ pub ( crate ) struct TableCreationConfig {
601+ /// Features explicitly enabled via `delta.feature.<name> = supported`.
602+ /// Contains only ReaderWriter features.
603+ pub ( crate ) reader_features : Vec < TableFeature > ,
604+ /// Features explicitly enabled via `delta.feature.<name> = supported`.
605+ /// Contains all features (Writer-only + ReaderWriter).
606+ pub ( crate ) writer_features : Vec < TableFeature > ,
607+ /// Protocol reader version from `delta.minReaderVersion` (if provided).
608+ pub ( crate ) min_reader_version : Option < i32 > ,
609+ /// Protocol writer version from `delta.minWriterVersion` (if provided).
610+ pub ( crate ) min_writer_version : Option < i32 > ,
611+ /// Remaining properties to store in Metadata.configuration.
612+ /// Signal flags are stripped out.
613+ pub ( crate ) table_properties : std:: collections:: HashMap < String , String > ,
614+ }
615+
616+ impl TryFrom < std:: collections:: HashMap < String , String > > for TableCreationConfig {
617+ type Error = Error ;
618+
619+ fn try_from ( properties : std:: collections:: HashMap < String , String > ) -> DeltaResult < Self > {
620+ use crate :: table_features:: {
621+ SET_TABLE_FEATURE_SUPPORTED_PREFIX , SET_TABLE_FEATURE_SUPPORTED_VALUE ,
622+ } ;
623+ use crate :: table_properties:: { MIN_READER_VERSION_PROP , MIN_WRITER_VERSION_PROP } ;
624+
625+ let mut config = TableCreationConfig :: default ( ) ;
626+ let mut all_features = Vec :: new ( ) ;
627+
628+ for ( key, value) in properties {
629+ if let Some ( feature_name) = key. strip_prefix ( SET_TABLE_FEATURE_SUPPORTED_PREFIX ) {
630+ // Parse delta.feature.* signal flags
631+ if value != SET_TABLE_FEATURE_SUPPORTED_VALUE {
632+ return Err ( Error :: generic ( format ! (
633+ "Invalid value '{}' for '{}'. Only '{}' is allowed." ,
634+ value, key, SET_TABLE_FEATURE_SUPPORTED_VALUE
635+ ) ) ) ;
636+ }
637+ let feature = TableFeature :: from_name ( feature_name) ;
638+ if matches ! ( feature, TableFeature :: Unknown ( _) ) {
639+ return Err ( Error :: generic ( format ! (
640+ "Unknown table feature '{}'. Cannot create table with unsupported features." ,
641+ feature_name
642+ ) ) ) ;
643+ }
644+ all_features. push ( feature) ;
645+ } else if key == MIN_READER_VERSION_PROP {
646+ // Parse delta.minReaderVersion
647+ let version: i32 = value. parse ( ) . map_err ( |_| {
648+ Error :: generic ( format ! (
649+ "Invalid value '{}' for '{}'. Must be an integer." ,
650+ value, key
651+ ) )
652+ } ) ?;
653+ config. min_reader_version = Some ( version) ;
654+ } else if key == MIN_WRITER_VERSION_PROP {
655+ // Parse delta.minWriterVersion
656+ let version: i32 = value. parse ( ) . map_err ( |_| {
657+ Error :: generic ( format ! (
658+ "Invalid value '{}' for '{}'. Must be an integer." ,
659+ value, key
660+ ) )
661+ } ) ?;
662+ config. min_writer_version = Some ( version) ;
663+ } else {
664+ // Pass through to table properties
665+ config. table_properties . insert ( key, value) ;
666+ }
667+ }
668+
669+ // Partition features: ReaderWriter -> reader list, all -> writer list
670+ config. reader_features = all_features
671+ . iter ( )
672+ . filter ( |f| f. is_reader_writer ( ) )
673+ . cloned ( )
674+ . collect ( ) ;
675+ config. writer_features = all_features;
676+
677+ Ok ( config)
678+ }
679+ }
680+
681+ impl TableCreationConfig {
682+ /// Delta properties allowed during CREATE TABLE.
683+ /// Expand this list as more features are supported (e.g., column mapping, clustering).
684+ const ALLOWED_DELTA_PROPERTIES : & [ & str ] = & [
685+ // Currently empty - no delta.* properties allowed yet
686+ // Future: "delta.enableDeletionVectors", "delta.columnMapping.mode", etc.
687+ ] ;
688+
689+ /// Table features allowed during CREATE TABLE.
690+ /// Expand this list as more features are supported.
691+ const ALLOWED_DELTA_FEATURES : & [ TableFeature ] = & [
692+ // Currently empty - no explicit feature overrides allowed yet
693+ // Future: TableFeature::DeletionVectors, TableFeature::ColumnMapping, etc.
694+ ] ;
695+
696+ /// Validates the configuration for CREATE TABLE.
697+ ///
698+ /// Checks:
699+ /// - Protocol versions (if specified) must be (3, 7)
700+ /// - Only allowed delta properties can be set
701+ /// - Only allowed features can be explicitly enabled
702+ pub ( crate ) fn validate_for_create ( & self ) -> DeltaResult < ( ) > {
703+ use crate :: table_features:: {
704+ TABLE_FEATURES_MIN_READER_VERSION , TABLE_FEATURES_MIN_WRITER_VERSION ,
705+ } ;
706+ use crate :: table_properties:: {
707+ DELTA_PROPERTY_PREFIX , MIN_READER_VERSION_PROP , MIN_WRITER_VERSION_PROP ,
708+ } ;
709+
710+ // Validate protocol versions
711+ if let Some ( v) = self . min_reader_version {
712+ if v != TABLE_FEATURES_MIN_READER_VERSION {
713+ return Err ( Error :: generic ( format ! (
714+ "Invalid value '{}' for '{}'. Only '{}' is supported." ,
715+ v, MIN_READER_VERSION_PROP , TABLE_FEATURES_MIN_READER_VERSION
716+ ) ) ) ;
717+ }
718+ }
719+ if let Some ( v) = self . min_writer_version {
720+ if v != TABLE_FEATURES_MIN_WRITER_VERSION {
721+ return Err ( Error :: generic ( format ! (
722+ "Invalid value '{}' for '{}'. Only '{}' is supported." ,
723+ v, MIN_WRITER_VERSION_PROP , TABLE_FEATURES_MIN_WRITER_VERSION
724+ ) ) ) ;
725+ }
726+ }
727+
728+ // Validate delta properties against allow-list
729+ for key in self . table_properties . keys ( ) {
730+ if key. starts_with ( DELTA_PROPERTY_PREFIX )
731+ && !Self :: ALLOWED_DELTA_PROPERTIES . contains ( & key. as_str ( ) )
732+ {
733+ return Err ( Error :: generic ( format ! (
734+ "Setting delta property '{}' is not supported during CREATE TABLE" ,
735+ key
736+ ) ) ) ;
737+ }
738+ }
739+
740+ // Validate features against allow-list
741+ for feature in & self . writer_features {
742+ if !Self :: ALLOWED_DELTA_FEATURES . contains ( feature) {
743+ return Err ( Error :: generic ( format ! (
744+ "Enabling feature '{}' is not supported during CREATE TABLE" ,
745+ feature
746+ ) ) ) ;
747+ }
748+ }
749+
750+ Ok ( ( ) )
751+ }
752+ }
753+
571754#[ cfg( test) ]
572755mod test {
573756 use std:: collections:: HashMap ;
@@ -1398,4 +1581,195 @@ mod test {
13981581 let config = create_mock_table_config ( & [ ] , & [ TableFeature :: CatalogOwnedPreview ] ) ;
13991582 assert ! ( config. ensure_operation_supported( Operation :: Write ) . is_ok( ) ) ;
14001583 }
1584+
1585+ // TableCreationConfig tests
1586+
1587+ #[ test]
1588+ fn test_table_creation_config_basic ( ) {
1589+ use super :: TableCreationConfig ;
1590+
1591+ let props = HashMap :: from ( [
1592+ (
1593+ "delta.feature.deletionVectors" . to_string ( ) ,
1594+ "supported" . to_string ( ) ,
1595+ ) ,
1596+ (
1597+ "delta.enableDeletionVectors" . to_string ( ) ,
1598+ "true" . to_string ( ) ,
1599+ ) ,
1600+ ] ) ;
1601+
1602+ let config = TableCreationConfig :: try_from ( props) . unwrap ( ) ;
1603+
1604+ // DeletionVectors is a ReaderWriter feature
1605+ assert_eq ! ( config. reader_features. len( ) , 1 ) ;
1606+ assert_eq ! ( config. reader_features[ 0 ] , TableFeature :: DeletionVectors ) ;
1607+ assert_eq ! ( config. writer_features. len( ) , 1 ) ;
1608+ assert_eq ! ( config. writer_features[ 0 ] , TableFeature :: DeletionVectors ) ;
1609+
1610+ // Feature override should be removed from table_properties
1611+ assert ! ( !config
1612+ . table_properties
1613+ . contains_key( "delta.feature.deletionVectors" ) ) ;
1614+ // Regular property should be retained
1615+ assert_eq ! (
1616+ config. table_properties. get( "delta.enableDeletionVectors" ) ,
1617+ Some ( & "true" . to_string( ) )
1618+ ) ;
1619+ }
1620+
1621+ #[ test]
1622+ fn test_table_creation_config_multiple_features ( ) {
1623+ use super :: TableCreationConfig ;
1624+
1625+ let props = HashMap :: from ( [
1626+ (
1627+ "delta.feature.deletionVectors" . to_string ( ) ,
1628+ "supported" . to_string ( ) ,
1629+ ) ,
1630+ (
1631+ "delta.feature.changeDataFeed" . to_string ( ) ,
1632+ "supported" . to_string ( ) ,
1633+ ) ,
1634+ (
1635+ "delta.feature.appendOnly" . to_string ( ) ,
1636+ "supported" . to_string ( ) ,
1637+ ) ,
1638+ (
1639+ "delta.enableDeletionVectors" . to_string ( ) ,
1640+ "true" . to_string ( ) ,
1641+ ) ,
1642+ ] ) ;
1643+
1644+ let config = TableCreationConfig :: try_from ( props) . unwrap ( ) ;
1645+
1646+ // All 3 features go into writer_features
1647+ assert_eq ! ( config. writer_features. len( ) , 3 ) ;
1648+ assert ! ( config
1649+ . writer_features
1650+ . contains( & TableFeature :: DeletionVectors ) ) ;
1651+ assert ! ( config
1652+ . writer_features
1653+ . contains( & TableFeature :: ChangeDataFeed ) ) ;
1654+ assert ! ( config. writer_features. contains( & TableFeature :: AppendOnly ) ) ;
1655+
1656+ // Only ReaderWriter features go into reader_features (DeletionVectors)
1657+ // ChangeDataFeed and AppendOnly are Writer-only features
1658+ assert_eq ! ( config. reader_features. len( ) , 1 ) ;
1659+ assert ! ( config
1660+ . reader_features
1661+ . contains( & TableFeature :: DeletionVectors ) ) ;
1662+
1663+ // Only the regular property should remain
1664+ assert_eq ! ( config. table_properties. len( ) , 1 ) ;
1665+ assert_eq ! (
1666+ config. table_properties. get( "delta.enableDeletionVectors" ) ,
1667+ Some ( & "true" . to_string( ) )
1668+ ) ;
1669+ }
1670+
1671+ #[ test]
1672+ fn test_table_creation_config_no_features ( ) {
1673+ use super :: TableCreationConfig ;
1674+
1675+ let props = HashMap :: from ( [
1676+ (
1677+ "delta.enableDeletionVectors" . to_string ( ) ,
1678+ "true" . to_string ( ) ,
1679+ ) ,
1680+ ( "delta.appendOnly" . to_string ( ) , "true" . to_string ( ) ) ,
1681+ ( "custom.property" . to_string ( ) , "value" . to_string ( ) ) ,
1682+ ] ) ;
1683+
1684+ let config = TableCreationConfig :: try_from ( props) . unwrap ( ) ;
1685+
1686+ assert ! ( config. reader_features. is_empty( ) ) ;
1687+ assert ! ( config. writer_features. is_empty( ) ) ;
1688+ assert_eq ! ( config. table_properties. len( ) , 3 ) ;
1689+ }
1690+
1691+ #[ test]
1692+ fn test_table_creation_config_parsing_failures ( ) {
1693+ use super :: TableCreationConfig ;
1694+
1695+ // Invalid feature value ("enabled" instead of "supported")
1696+ let props = HashMap :: from ( [ (
1697+ "delta.feature.deletionVectors" . to_string ( ) ,
1698+ "enabled" . to_string ( ) ,
1699+ ) ] ) ;
1700+ assert_result_error_with_message (
1701+ TableCreationConfig :: try_from ( props) ,
1702+ "Invalid value 'enabled'" ,
1703+ ) ;
1704+
1705+ // Unknown feature name
1706+ let props = HashMap :: from ( [ (
1707+ "delta.feature.futureFeature" . to_string ( ) ,
1708+ "supported" . to_string ( ) ,
1709+ ) ] ) ;
1710+ assert_result_error_with_message (
1711+ TableCreationConfig :: try_from ( props) ,
1712+ "Unknown table feature 'futureFeature'" ,
1713+ ) ;
1714+ }
1715+
1716+ #[ test]
1717+ fn test_table_creation_config_protocol_version_handling ( ) {
1718+ use super :: TableCreationConfig ;
1719+ use crate :: table_properties:: { MIN_READER_VERSION_PROP , MIN_WRITER_VERSION_PROP } ;
1720+
1721+ // Valid protocol versions: parsing and validation
1722+ let props = HashMap :: from ( [
1723+ ( MIN_READER_VERSION_PROP . to_string ( ) , "3" . to_string ( ) ) ,
1724+ ( MIN_WRITER_VERSION_PROP . to_string ( ) , "7" . to_string ( ) ) ,
1725+ ( "custom.property" . to_string ( ) , "value" . to_string ( ) ) ,
1726+ ] ) ;
1727+ let config = TableCreationConfig :: try_from ( props) . unwrap ( ) ;
1728+ // Versions correctly extracted
1729+ assert_eq ! ( config. min_reader_version, Some ( 3 ) ) ;
1730+ assert_eq ! ( config. min_writer_version, Some ( 7 ) ) ;
1731+ // Protocol version properties stripped from table_properties
1732+ assert ! ( !config
1733+ . table_properties
1734+ . contains_key( MIN_READER_VERSION_PROP ) ) ;
1735+ assert ! ( !config
1736+ . table_properties
1737+ . contains_key( MIN_WRITER_VERSION_PROP ) ) ;
1738+ // Other properties remain
1739+ assert_eq ! (
1740+ config. table_properties. get( "custom.property" ) ,
1741+ Some ( & "value" . to_string( ) )
1742+ ) ;
1743+ // Validation passes
1744+ assert ! ( config. validate_for_create( ) . is_ok( ) ) ;
1745+
1746+ // Invalid reader version (only 3 supported)
1747+ let props = HashMap :: from ( [ ( MIN_READER_VERSION_PROP . to_string ( ) , "2" . to_string ( ) ) ] ) ;
1748+ let config = TableCreationConfig :: try_from ( props) . unwrap ( ) ;
1749+ assert_result_error_with_message ( config. validate_for_create ( ) , "Only '3' is supported" ) ;
1750+
1751+ // Invalid writer version (only 7 supported)
1752+ let props = HashMap :: from ( [ ( MIN_WRITER_VERSION_PROP . to_string ( ) , "5" . to_string ( ) ) ] ) ;
1753+ let config = TableCreationConfig :: try_from ( props) . unwrap ( ) ;
1754+ assert_result_error_with_message ( config. validate_for_create ( ) , "Only '7' is supported" ) ;
1755+
1756+ // Features not on allow list
1757+ let props = HashMap :: from ( [ (
1758+ "delta.feature.deletionVectors" . to_string ( ) ,
1759+ "supported" . to_string ( ) ,
1760+ ) ] ) ;
1761+ let config = TableCreationConfig :: try_from ( props) . unwrap ( ) ;
1762+ assert_result_error_with_message (
1763+ config. validate_for_create ( ) ,
1764+ "Enabling feature 'deletionVectors' is not supported" ,
1765+ ) ;
1766+
1767+ // Delta properties not on allow list
1768+ let props = HashMap :: from ( [ ( "delta.appendOnly" . to_string ( ) , "true" . to_string ( ) ) ] ) ;
1769+ let config = TableCreationConfig :: try_from ( props) . unwrap ( ) ;
1770+ assert_result_error_with_message (
1771+ config. validate_for_create ( ) ,
1772+ "Setting delta property 'delta.appendOnly' is not supported" ,
1773+ ) ;
1774+ }
14011775}
0 commit comments