Skip to content

Commit 83007bc

Browse files
committed
feat: Add support for delta.feature.X = supported table feature overrides
Add the ability to explicitly enable table features via table properties using the delta.feature.<featureName> = supported syntax, matching the Java Kernel's behavior. Key Changes: - Add SET_TABLE_FEATURE_SUPPORTED_PREFIX and SET_TABLE_FEATURE_SUPPORTED_VALUE constants to table_features module - Add TableFeature::from_name() to parse feature names from strings - Add TableFeature::is_reader_writer() to check feature type - Add extract_feature_overrides() function that: - Extracts delta.feature.* properties from table properties - Validates the value is 'supported' - Returns features to add to protocol and cleaned properties - Update CreateTableBuilder::build() to: - Call extract_feature_overrides() on table properties - Add extracted features to Protocol's reader/writer feature lists - Pass cleaned properties (without feature overrides) to Metadata Usage: let result = TableManager::create_table(path, schema, "MyApp/1.0") .with_table_properties(HashMap::from([ ("delta.feature.deletionVectors".to_string(), "supported".to_string()), ("delta.enableDeletionVectors".to_string(), "true".to_string()), ])) .build(engine, committer)? .commit(engine)?; The delta.feature.* properties are consumed during build() and not stored in the final Metadata configuration, matching Java Kernel behavior.
1 parent 3a2fe86 commit 83007bc

File tree

3 files changed

+418
-13
lines changed

3 files changed

+418
-13
lines changed

kernel/src/table_features/mod.rs

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
use std::collections::HashMap;
2+
use std::str::FromStr;
3+
14
use serde::{Deserialize, Serialize};
25
use strum::{AsRefStr, Display as StrumDisplay, EnumCount, EnumString};
36

@@ -15,6 +18,15 @@ pub(crate) use timestamp_ntz::validate_timestamp_ntz_feature_support;
1518
mod column_mapping;
1619
mod timestamp_ntz;
1720

21+
/// Prefix for table feature override properties.
22+
/// Properties with this prefix (e.g., `delta.feature.deletionVectors`) are used to
23+
/// explicitly enable table features in the protocol.
24+
pub const SET_TABLE_FEATURE_SUPPORTED_PREFIX: &str = "delta.feature.";
25+
26+
/// Value to enable a table feature when used with [`SET_TABLE_FEATURE_SUPPORTED_PREFIX`].
27+
/// Example: `"delta.feature.deletionVectors" -> "supported"`
28+
pub const SET_TABLE_FEATURE_SUPPORTED_VALUE: &str = "supported";
29+
1830
/// Table features represent protocol capabilities required to correctly read or write a given table.
1931
/// - Readers must implement all features required for correct table reads.
2032
/// - Writers must implement all features required for correct table writes.
@@ -646,6 +658,91 @@ impl TableFeature {
646658
TableFeature::Unknown(_) => None,
647659
}
648660
}
661+
662+
/// Parse a feature name string into a TableFeature.
663+
///
664+
/// This uses the strum `EnumString` derive to parse known feature names.
665+
/// Unknown feature names are wrapped in `TableFeature::Unknown`.
666+
pub(crate) fn from_name(name: &str) -> Self {
667+
TableFeature::from_str(name).unwrap_or_else(|_| TableFeature::Unknown(name.to_string()))
668+
}
669+
670+
/// Returns true if this is a ReaderWriter feature (appears in both reader and writer feature lists).
671+
/// Returns false for Writer-only features and Unknown features.
672+
pub(crate) fn is_reader_writer(&self) -> bool {
673+
matches!(self.feature_type(), FeatureType::ReaderWriter)
674+
}
675+
}
676+
677+
/// Result of extracting feature overrides from table properties.
678+
///
679+
/// Contains the features to add to the protocol and the cleaned properties
680+
/// (with `delta.feature.*` entries removed).
681+
#[derive(Debug)]
682+
pub(crate) struct FeatureOverrides {
683+
/// Features that should be added to the protocol
684+
pub(crate) features: Vec<TableFeature>,
685+
/// Table properties with `delta.feature.*` entries removed
686+
pub(crate) cleaned_properties: HashMap<String, String>,
687+
}
688+
689+
/// Extracts feature overrides from table properties.
690+
///
691+
/// Properties with the prefix `delta.feature.` are interpreted as feature override
692+
/// directives. For example, `delta.feature.deletionVectors = supported` adds the
693+
/// `deletionVectors` feature to the protocol.
694+
///
695+
/// # Arguments
696+
/// * `properties` - The table properties to process
697+
///
698+
/// # Returns
699+
/// A [`FeatureOverrides`] containing:
700+
/// - `features`: The list of features to add to the protocol
701+
/// - `cleaned_properties`: The table properties with feature override entries removed
702+
///
703+
/// # Errors
704+
/// Returns an error if:
705+
/// - A `delta.feature.*` property has a value other than "supported"
706+
///
707+
/// # Example
708+
/// ```ignore
709+
/// let props = HashMap::from([
710+
/// ("delta.feature.deletionVectors".to_string(), "supported".to_string()),
711+
/// ("delta.enableDeletionVectors".to_string(), "true".to_string()),
712+
/// ]);
713+
/// let overrides = extract_feature_overrides(props)?;
714+
/// // overrides.features = [TableFeature::DeletionVectors]
715+
/// // overrides.cleaned_properties = {"delta.enableDeletionVectors": "true"}
716+
/// ```
717+
pub(crate) fn extract_feature_overrides(
718+
properties: HashMap<String, String>,
719+
) -> DeltaResult<FeatureOverrides> {
720+
let mut features = Vec::new();
721+
let mut cleaned_properties = HashMap::new();
722+
723+
for (key, value) in properties {
724+
if let Some(feature_name) = key.strip_prefix(SET_TABLE_FEATURE_SUPPORTED_PREFIX) {
725+
// Validate value is "supported"
726+
if value != SET_TABLE_FEATURE_SUPPORTED_VALUE {
727+
return Err(Error::generic(format!(
728+
"Invalid value '{}' for '{}'. Only '{}' is allowed.",
729+
value, key, SET_TABLE_FEATURE_SUPPORTED_VALUE
730+
)));
731+
}
732+
733+
// Parse feature name
734+
let feature = TableFeature::from_name(feature_name);
735+
features.push(feature);
736+
} else {
737+
// Keep non-feature-override properties
738+
cleaned_properties.insert(key, value);
739+
}
740+
}
741+
742+
Ok(FeatureOverrides {
743+
features,
744+
cleaned_properties,
745+
})
649746
}
650747

651748
impl ToDataType for TableFeature {
@@ -786,4 +883,163 @@ mod tests {
786883
assert_eq!(from_str, feature);
787884
}
788885
}
886+
887+
#[test]
888+
fn test_from_name() {
889+
// Known features
890+
assert_eq!(
891+
TableFeature::from_name("deletionVectors"),
892+
TableFeature::DeletionVectors
893+
);
894+
assert_eq!(
895+
TableFeature::from_name("changeDataFeed"),
896+
TableFeature::ChangeDataFeed
897+
);
898+
assert_eq!(
899+
TableFeature::from_name("columnMapping"),
900+
TableFeature::ColumnMapping
901+
);
902+
assert_eq!(
903+
TableFeature::from_name("timestampNtz"),
904+
TableFeature::TimestampWithoutTimezone
905+
);
906+
907+
// Unknown features
908+
assert_eq!(
909+
TableFeature::from_name("unknownFeature"),
910+
TableFeature::Unknown("unknownFeature".to_string())
911+
);
912+
}
913+
914+
#[test]
915+
fn test_is_reader_writer() {
916+
// ReaderWriter features
917+
assert!(TableFeature::DeletionVectors.is_reader_writer());
918+
assert!(TableFeature::ColumnMapping.is_reader_writer());
919+
assert!(TableFeature::TimestampWithoutTimezone.is_reader_writer());
920+
assert!(TableFeature::V2Checkpoint.is_reader_writer());
921+
922+
// Writer-only features
923+
assert!(!TableFeature::ChangeDataFeed.is_reader_writer());
924+
assert!(!TableFeature::AppendOnly.is_reader_writer());
925+
assert!(!TableFeature::DomainMetadata.is_reader_writer());
926+
assert!(!TableFeature::RowTracking.is_reader_writer());
927+
928+
// Unknown features
929+
assert!(!TableFeature::unknown("something").is_reader_writer());
930+
}
931+
932+
#[test]
933+
fn test_extract_feature_overrides_basic() {
934+
let props = HashMap::from([
935+
(
936+
"delta.feature.deletionVectors".to_string(),
937+
"supported".to_string(),
938+
),
939+
(
940+
"delta.enableDeletionVectors".to_string(),
941+
"true".to_string(),
942+
),
943+
]);
944+
945+
let result = extract_feature_overrides(props).unwrap();
946+
947+
assert_eq!(result.features.len(), 1);
948+
assert_eq!(result.features[0], TableFeature::DeletionVectors);
949+
950+
// Feature override should be removed from cleaned properties
951+
assert!(!result
952+
.cleaned_properties
953+
.contains_key("delta.feature.deletionVectors"));
954+
// Regular property should be retained
955+
assert_eq!(
956+
result.cleaned_properties.get("delta.enableDeletionVectors"),
957+
Some(&"true".to_string())
958+
);
959+
}
960+
961+
#[test]
962+
fn test_extract_feature_overrides_multiple_features() {
963+
let props = HashMap::from([
964+
(
965+
"delta.feature.deletionVectors".to_string(),
966+
"supported".to_string(),
967+
),
968+
(
969+
"delta.feature.changeDataFeed".to_string(),
970+
"supported".to_string(),
971+
),
972+
(
973+
"delta.feature.appendOnly".to_string(),
974+
"supported".to_string(),
975+
),
976+
(
977+
"delta.enableDeletionVectors".to_string(),
978+
"true".to_string(),
979+
),
980+
]);
981+
982+
let result = extract_feature_overrides(props).unwrap();
983+
984+
assert_eq!(result.features.len(), 3);
985+
assert!(result.features.contains(&TableFeature::DeletionVectors));
986+
assert!(result.features.contains(&TableFeature::ChangeDataFeed));
987+
assert!(result.features.contains(&TableFeature::AppendOnly));
988+
989+
// Only the regular property should remain
990+
assert_eq!(result.cleaned_properties.len(), 1);
991+
assert_eq!(
992+
result.cleaned_properties.get("delta.enableDeletionVectors"),
993+
Some(&"true".to_string())
994+
);
995+
}
996+
997+
#[test]
998+
fn test_extract_feature_overrides_invalid_value() {
999+
let props = HashMap::from([(
1000+
"delta.feature.deletionVectors".to_string(),
1001+
"enabled".to_string(), // Wrong value - should be "supported"
1002+
)]);
1003+
1004+
let result = extract_feature_overrides(props);
1005+
1006+
assert!(result.is_err());
1007+
let err = result.unwrap_err().to_string();
1008+
assert!(err.contains("Invalid value"));
1009+
assert!(err.contains("enabled"));
1010+
assert!(err.contains("supported"));
1011+
}
1012+
1013+
#[test]
1014+
fn test_extract_feature_overrides_no_features() {
1015+
let props = HashMap::from([
1016+
(
1017+
"delta.enableDeletionVectors".to_string(),
1018+
"true".to_string(),
1019+
),
1020+
("delta.appendOnly".to_string(), "true".to_string()),
1021+
("custom.property".to_string(), "value".to_string()),
1022+
]);
1023+
1024+
let result = extract_feature_overrides(props).unwrap();
1025+
1026+
assert!(result.features.is_empty());
1027+
assert_eq!(result.cleaned_properties.len(), 3);
1028+
}
1029+
1030+
#[test]
1031+
fn test_extract_feature_overrides_unknown_feature() {
1032+
let props = HashMap::from([(
1033+
"delta.feature.futureFeature".to_string(),
1034+
"supported".to_string(),
1035+
)]);
1036+
1037+
let result = extract_feature_overrides(props).unwrap();
1038+
1039+
assert_eq!(result.features.len(), 1);
1040+
assert_eq!(
1041+
result.features[0],
1042+
TableFeature::Unknown("futureFeature".to_string())
1043+
);
1044+
}
7891045
}

kernel/src/transaction/create_table.rs

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ use crate::actions::{
3636
};
3737
use crate::committer::Committer;
3838
use crate::schema::SchemaRef;
39+
use crate::table_features::extract_feature_overrides;
3940
use crate::transaction::Transaction;
4041
use crate::utils::{current_time_ms, try_parse_uri};
4142
use crate::{DeltaResult, Engine, Error};
@@ -172,25 +173,44 @@ impl CreateTableBuilder {
172173
}
173174
}
174175

176+
// Extract feature overrides (delta.feature.X = supported) from table properties
177+
let feature_overrides = extract_feature_overrides(self.table_properties)?;
178+
179+
// Build reader/writer feature lists from explicit feature overrides
180+
let mut reader_features: Vec<String> = Vec::new();
181+
let mut writer_features: Vec<String> = Vec::new();
182+
183+
for feature in &feature_overrides.features {
184+
let feature_name = feature.to_string();
185+
186+
// All features go into writer_features
187+
writer_features.push(feature_name.clone());
188+
189+
// ReaderWriter features also go into reader_features
190+
if feature.is_reader_writer() {
191+
reader_features.push(feature_name);
192+
}
193+
}
194+
175195
// Create Protocol action with table features support
176196
let protocol = Protocol::try_new(
177197
TABLE_FEATURES_MIN_READER_VERSION,
178198
TABLE_FEATURES_MIN_WRITER_VERSION,
179-
Some(Vec::<String>::new()), // readerFeatures (empty for now)
180-
Some(Vec::<String>::new()), // writerFeatures (empty for now)
199+
Some(reader_features),
200+
Some(writer_features),
181201
)?;
182202

183203
// Get current timestamp
184204
let created_time = current_time_ms()?;
185205

186-
// Create Metadata action
206+
// Create Metadata action with cleaned properties (feature overrides removed)
187207
let metadata = Metadata::try_new(
188208
None, // name
189209
None, // description
190210
(*self.schema).clone(),
191211
self.partition_columns,
192212
created_time,
193-
self.table_properties,
213+
feature_overrides.cleaned_properties,
194214
)?;
195215

196216
// Create Transaction with cached Protocol and Metadata

0 commit comments

Comments
 (0)