Skip to content

Commit 9bb9ade

Browse files
sanujbasuSanuj Basu
authored andcommitted
feat: Introduce TableCreationConfig for table creation property handling
1. Adds the ability to explicitly enable table features via table properties using the delta.feature.<featureName> = supported syntax, matching the Java Kernel's behavior. Only ALLOWED_DELTA_FEATURES can be set during create. Features get added to protocol features. 2. Allows the min reader/ writer versions to be updated in the protocol using signal flags. Only protocol versions (3, 7) are supported. 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 TableCreationConfig struct to encapsulate parsing and validation of user-provided table properties during CREATE TABLE operations. - Extract delta.feature.* signal flags into reader/writer feature lists - Extract delta.minReaderVersion/minWriterVersion into protocol hints - Strip signal flags from properties, pass remaining to metadata - Reject unknown features and invalid feature flag values Usage: create_table("/path/to/table", schema, "MyApp/1.0") .with_table_properties([ ("delta.minReaderVersion", "3"), ("delta.minWriterVersion", "7"), ]) .build(&engine, Box::new(FileSystemCommitter::new()))? .commit(&engine)?; The delta.feature.* properties are consumed during build() and not stored in the final Metadata configuration, matching Java Kernel behavior.
1 parent e891a95 commit 9bb9ade

File tree

5 files changed

+670
-76
lines changed

5 files changed

+670
-76
lines changed

kernel/src/table_configuration.rs

Lines changed: 374 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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)]
572755
mod 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

Comments
 (0)