Skip to content

Commit d2ba5ea

Browse files
authored
feat: Unify TableFeatures followups (delta-io#1404)
## What changes are proposed in this pull request? <!-- Please clarify what changes you are proposing and why the changes are needed. The purpose of this section is to outline the changes, why they are needed, and how this PR fixes the issue. If the reason for the change is already explained clearly in an issue, then it does not need to be restated here. 1. If you propose a new API or feature, clarify the use case for a new API or feature. 2. If you fix a bug, you can clarify why it is a bug. --> This PR does a followup to unifying TableFeatures followups. 1. refactors some functions around for testing 2. Failing on Unknown Table features 3. Enabled ignored test by removing column mapping ## How was this change tested? <!-- Please make sure to add test cases that check the changes thoroughly including negative and positive cases if possible. If it was tested in a way different from regular unit tests, please clarify how you tested, ideally via a reproducible test documented in the PR description. --> Fix all tests to use the unified table features
1 parent 12e93a4 commit d2ba5ea

4 files changed

Lines changed: 127 additions & 88 deletions

File tree

kernel/src/actions/mod.rs

Lines changed: 85 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -402,23 +402,6 @@ impl Protocol {
402402
reader_features: Option<impl IntoIterator<Item = impl ToString>>,
403403
writer_features: Option<impl IntoIterator<Item = impl ToString>>,
404404
) -> DeltaResult<Self> {
405-
if min_reader_version == 3 {
406-
require!(
407-
reader_features.is_some(),
408-
Error::invalid_protocol(
409-
"Reader features must be present when minimum reader version = 3"
410-
)
411-
);
412-
}
413-
if min_writer_version == 7 {
414-
require!(
415-
writer_features.is_some(),
416-
Error::invalid_protocol(
417-
"Writer features must be present when minimum writer version = 7"
418-
)
419-
);
420-
}
421-
422405
let reader_features = parse_features(reader_features);
423406
let writer_features = parse_features(writer_features);
424407

@@ -474,9 +457,44 @@ impl Protocol {
474457

475458
/// Validates the relationship between reader features and writer features in the protocol.
476459
pub(crate) fn validate_table_features(&self) -> DeltaResult<()> {
460+
// The protocol states that Reader features may be present if and only if the min_reader_version is 3
461+
if self.min_reader_version == 3 {
462+
require!(
463+
self.reader_features.is_some(),
464+
Error::invalid_protocol(
465+
"Reader features must be present when minimum reader version = 3"
466+
)
467+
);
468+
} else {
469+
require!(
470+
self.reader_features.is_none(),
471+
Error::invalid_protocol(
472+
"Reader features must not be present when minimum reader version != 3"
473+
)
474+
);
475+
}
476+
477+
// The protocol states that Writer features may be present if and only if the min_writer_version is 7
478+
if self.min_writer_version == 7 {
479+
require!(
480+
self.writer_features.is_some(),
481+
Error::invalid_protocol(
482+
"Writer features must be present when minimum writer version = 7"
483+
)
484+
);
485+
} else {
486+
require!(
487+
self.writer_features.is_none(),
488+
Error::invalid_protocol(
489+
"Writer features must not be present when minimum writer version != 7"
490+
)
491+
);
492+
}
493+
477494
match (&self.reader_features, &self.writer_features) {
478495
(Some(reader_features), Some(writer_features)) => {
479496
// Check all reader features are ReaderWriter and present in writer features.
497+
// Unknown features are treated as potentially ReaderWriter for forward compatibility.
480498
let check_r = reader_features.iter().all(|feature| {
481499
matches!(
482500
feature.feature_type(),
@@ -490,13 +508,14 @@ impl Protocol {
490508
)
491509
);
492510

493-
// Check all writer features are either Writer or also in reader features (ReaderWriter).
494-
let check_w = writer_features.iter().all(|feature| {
495-
matches!(
496-
feature.feature_type(),
497-
FeatureType::Writer | FeatureType::Unknown
498-
) || reader_features.contains(feature)
499-
});
511+
// Check all writer features that are ReaderWriter must also be in reader features
512+
// Unknown features are treated as potentially Writer-only for forward compatibility.
513+
let check_w = writer_features
514+
.iter()
515+
.all(|feature| match feature.feature_type() {
516+
FeatureType::Writer | FeatureType::Unknown => true,
517+
FeatureType::ReaderWriter => reader_features.contains(feature),
518+
});
500519
require!(
501520
check_w,
502521
Error::invalid_protocol(
@@ -507,6 +526,7 @@ impl Protocol {
507526
}
508527
(None, None) => Ok(()),
509528
(None, Some(writer_features)) => {
529+
// Unknown features are treated as potentially Writer-only for forward compatibility.
510530
require!(
511531
writer_features.iter().all(|feature| matches!(
512532
feature.feature_type(),
@@ -1373,9 +1393,9 @@ mod tests {
13731393
Some(vec![TableFeature::AppendOnly]),
13741394
"Reader features must contain only ReaderWriter features that are also listed in writer features",
13751395
),
1376-
(Some(vec![TableFeature::DeletionVectors]), None, "Reader features should be present in writer features"),
1396+
(Some(vec![TableFeature::DeletionVectors]), Some(vec![]), "Reader features must contain only ReaderWriter features that are also listed in writer features"),
13771397
// ReaderWriter feature not present in reader features
1378-
(None, Some(vec![TableFeature::DeletionVectors]), "Writer features must be Writer-only or also listed in reader features"),
1398+
(Some(vec![]), Some(vec![TableFeature::DeletionVectors]), "Writer features must be Writer-only or also listed in reader features"),
13791399
(
13801400
Some(vec![TableFeature::VariantType]),
13811401
Some(vec![
@@ -1390,12 +1410,6 @@ mod tests {
13901410
Some(vec![TableFeature::AppendOnly]),
13911411
"Reader features must contain only ReaderWriter features that are also listed in writer features",
13921412
),
1393-
// Reader only feature is not allowed
1394-
(
1395-
Some(vec![TableFeature::Unknown("r".to_string())]),
1396-
Some(vec![TableFeature::Unknown("w".to_string())]),
1397-
"Reader features must contain only ReaderWriter features that are also listed in writer features",
1398-
),
13991413
];
14001414

14011415
for (reader_features, writer_features, error_msg) in invalid_features {
@@ -1417,6 +1431,40 @@ mod tests {
14171431
}
14181432
}
14191433

1434+
#[test]
1435+
fn test_validate_table_features_unknown() {
1436+
// Unknown features are allowed during validation for forward compatibility,
1437+
// but will be rejected when trying to use the protocol (ensure_read_supported/ensure_write_supported)
1438+
1439+
// Test unknown features in reader - validation passes
1440+
let protocol = Protocol {
1441+
min_reader_version: 3,
1442+
min_writer_version: 7,
1443+
reader_features: Some(vec![TableFeature::Unknown("unknown_reader".to_string())]),
1444+
writer_features: Some(vec![TableFeature::Unknown("unknown_reader".to_string())]),
1445+
};
1446+
assert!(protocol.validate_table_features().is_ok());
1447+
// But ensure_read_supported should fail
1448+
assert!(matches!(
1449+
protocol.ensure_read_supported(),
1450+
Err(Error::Unsupported(_))
1451+
));
1452+
1453+
// Test unknown features in writer - validation passes
1454+
let protocol = Protocol {
1455+
min_reader_version: 3,
1456+
min_writer_version: 7,
1457+
reader_features: Some(vec![]),
1458+
writer_features: Some(vec![TableFeature::Unknown("unknown_writer".to_string())]),
1459+
};
1460+
assert!(protocol.validate_table_features().is_ok());
1461+
// But ensure_write_supported should fail
1462+
assert!(matches!(
1463+
protocol.ensure_write_supported(),
1464+
Err(Error::Unsupported(_))
1465+
));
1466+
}
1467+
14201468
#[test]
14211469
fn test_validate_table_features_valid() {
14221470
// (reader_feature, writer_feature)
@@ -1427,12 +1475,12 @@ mod tests {
14271475
Some(vec![TableFeature::DeletionVectors]),
14281476
Some(vec![TableFeature::DeletionVectors]),
14291477
),
1430-
(None, Some(vec![TableFeature::AppendOnly])),
1478+
(Some(vec![]), Some(vec![TableFeature::AppendOnly])),
14311479
(
14321480
Some(vec![TableFeature::VariantType]),
14331481
Some(vec![TableFeature::VariantType, TableFeature::AppendOnly]),
14341482
),
1435-
// Unknwon feature may be ReaderWriter or Writer only
1483+
// Unknown feature may be ReaderWriter or Writer only (for forward compatibility)
14361484
(
14371485
Some(vec![TableFeature::Unknown("rw".to_string())]),
14381486
Some(vec![
@@ -1441,7 +1489,7 @@ mod tests {
14411489
]),
14421490
),
14431491
// Empty feature set is valid
1444-
(None, None),
1492+
(Some(vec![]), Some(vec![])),
14451493
];
14461494

14471495
for (reader_features, writer_features) in valid_features {
@@ -1534,7 +1582,8 @@ mod tests {
15341582
r#"Unsupported: Unknown TableFeatures: "identityColumns". Supported TableFeatures: "appendOnly", "deletionVectors", "domainMetadata", "inCommitTimestamp", "invariants", "rowTracking", "timestampNtz", "variantType", "variantType-preview", "variantShredding-preview""#,
15351583
);
15361584

1537-
// Unknown writer features should cause an error
1585+
// Unknown writer features are allowed during creation for forward compatibility,
1586+
// but will fail when trying to write
15381587
let protocol = Protocol::try_new(
15391588
3,
15401589
7,

kernel/src/table_changes/log_replay/tests.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -566,7 +566,7 @@ async fn failing_protocol() {
566566

567567
let protocol = Protocol::try_new(
568568
3,
569-
1,
569+
7,
570570
["fake_feature".to_string()].into(),
571571
["fake_feature".to_string()].into(),
572572
)

kernel/src/table_features/mod.rs

Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,9 @@ mod timestamp_ntz;
4141
#[serde(rename_all = "camelCase")]
4242
#[internal_api]
4343
pub(crate) enum TableFeature {
44-
/// CatalogManaged tables:
45-
/// <https://github.com/delta-io/delta/blob/master/protocol_rfcs/catalog-managed.md>
46-
CatalogManaged,
47-
#[strum(serialize = "catalogOwned-preview")]
48-
#[serde(rename = "catalogOwned-preview")]
49-
CatalogOwnedPreview,
44+
//////////////////////////
45+
// Writer-only features //
46+
//////////////////////////
5047
/// Append Only Tables
5148
AppendOnly,
5249
/// Table invariants
@@ -57,16 +54,37 @@ pub(crate) enum TableFeature {
5754
ChangeDataFeed,
5855
/// Columns with generated values
5956
GeneratedColumns,
60-
/// Mapping of one column to another
61-
ColumnMapping,
6257
/// ID Columns
6358
IdentityColumns,
6459
/// Monotonically increasing timestamps in the CommitInfo
6560
InCommitTimestamp,
66-
/// Deletion vectors for merge, update, delete
67-
DeletionVectors,
6861
/// Row tracking on tables
6962
RowTracking,
63+
/// domain specific metadata
64+
DomainMetadata,
65+
/// Iceberg compatibility support
66+
IcebergCompatV1,
67+
/// Iceberg compatibility support
68+
IcebergCompatV2,
69+
/// The Clustered Table feature facilitates the physical clustering of rows
70+
/// that share similar values on a predefined set of clustering columns.
71+
#[strum(serialize = "clustering")]
72+
#[serde(rename = "clustering")]
73+
ClusteredTable,
74+
75+
///////////////////////////
76+
// ReaderWriter features //
77+
///////////////////////////
78+
/// CatalogManaged tables:
79+
/// <https://github.com/delta-io/delta/blob/master/protocol_rfcs/catalog-managed.md>
80+
CatalogManaged,
81+
#[strum(serialize = "catalogOwned-preview")]
82+
#[serde(rename = "catalogOwned-preview")]
83+
CatalogOwnedPreview,
84+
/// Mapping of one column to another
85+
ColumnMapping,
86+
/// Deletion vectors for merge, update, delete
87+
DeletionVectors,
7088
/// timestamps without timezone support
7189
#[strum(serialize = "timestampNtz")]
7290
#[serde(rename = "timestampNtz")]
@@ -76,22 +94,11 @@ pub(crate) enum TableFeature {
7694
#[strum(serialize = "typeWidening-preview")]
7795
#[serde(rename = "typeWidening-preview")]
7896
TypeWideningPreview,
79-
/// domain specific metadata
80-
DomainMetadata,
8197
/// version 2 of checkpointing
8298
V2Checkpoint,
83-
/// Iceberg compatibility support
84-
IcebergCompatV1,
85-
/// Iceberg compatibility support
86-
IcebergCompatV2,
8799
/// vacuumProtocolCheck ReaderWriter feature ensures consistent application of reader and writer
88100
/// protocol checks during VACUUM operations
89101
VacuumProtocolCheck,
90-
/// The Clustered Table feature facilitates the physical clustering of rows
91-
/// that share similar values on a predefined set of clustering columns.
92-
#[strum(serialize = "clustering")]
93-
#[serde(rename = "clustering")]
94-
ClusteredTable,
95102
/// This feature enables support for the variant data type, which stores semi-structured data.
96103
VariantType,
97104
#[strum(serialize = "variantType-preview")]
@@ -100,15 +107,20 @@ pub(crate) enum TableFeature {
100107
#[strum(serialize = "variantShredding-preview")]
101108
#[serde(rename = "variantShredding-preview")]
102109
VariantShreddingPreview,
110+
103111
#[serde(untagged)]
104112
#[strum(default)]
105113
Unknown(String),
106114
}
107115

108-
#[derive(Eq, PartialEq)]
116+
/// Classifies table features by their type
117+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109118
pub(crate) enum FeatureType {
119+
/// Feature only affects write operations
110120
Writer,
121+
/// Feature affects both read and write operations (must appear in both feature lists)
111122
ReaderWriter,
123+
/// Unknown feature type (for forward compatibility)
112124
Unknown,
113125
}
114126

@@ -247,7 +259,7 @@ mod tests {
247259
}
248260

249261
#[test]
250-
fn test_roundtrip_reader_features() {
262+
fn test_roundtrip_table_features() {
251263
let cases = [
252264
(TableFeature::CatalogManaged, "catalogManaged"),
253265
(TableFeature::CatalogOwnedPreview, "catalogOwned-preview"),

kernel/tests/write.rs

Lines changed: 6 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -859,7 +859,6 @@ async fn test_append_timestamp_ntz() -> Result<(), Box<dyn std::error::Error>> {
859859
Ok(())
860860
}
861861

862-
#[ignore]
863862
#[tokio::test]
864863
async fn test_append_variant() -> Result<(), Box<dyn std::error::Error>> {
865864
// setup tracing
@@ -880,38 +879,20 @@ async fn test_append_variant() -> Result<(), Box<dyn std::error::Error>> {
880879

881880
// create a table with VARIANT column
882881
let table_schema = Arc::new(StructType::try_new(vec![
883-
StructField::nullable("v", DataType::unshredded_variant())
884-
.with_metadata([("delta.columnMapping.physicalName", "col1")])
885-
.add_metadata([("delta.columnMapping.id", 1)]),
886-
StructField::nullable("i", DataType::INTEGER)
887-
.with_metadata([("delta.columnMapping.physicalName", "col2")])
888-
.add_metadata([("delta.columnMapping.id", 2)]),
882+
StructField::nullable("v", DataType::unshredded_variant()),
883+
StructField::nullable("i", DataType::INTEGER),
889884
StructField::nullable(
890885
"nested",
891886
// We flip the value and metadata fields in the actual parquet file for the test
892887
StructType::try_new(vec![StructField::nullable(
893888
"nested_v",
894889
unshredded_variant_schema_flipped(),
895-
)
896-
.with_metadata([("delta.columnMapping.physicalName", "col21")])
897-
.add_metadata([("delta.columnMapping.id", 3)])])?,
898-
)
899-
.with_metadata([("delta.columnMapping.physicalName", "col3")])
900-
.add_metadata([("delta.columnMapping.id", 4)]),
901-
])?);
902-
903-
let write_schema = Arc::new(StructType::try_new(vec![
904-
StructField::nullable("col1", DataType::unshredded_variant()),
905-
StructField::nullable("col2", DataType::INTEGER),
906-
StructField::nullable(
907-
"col3",
908-
StructType::try_new(vec![StructField::nullable(
909-
"col21",
910-
unshredded_variant_schema_flipped(),
911890
)])?,
912891
),
913892
])?);
914893

894+
let write_schema = table_schema.clone();
895+
915896
let tmp_test_dir = tempdir()?;
916897
let tmp_test_dir_url = Url::from_directory_path(tmp_test_dir.path()).unwrap();
917898

@@ -921,16 +902,13 @@ async fn test_append_variant() -> Result<(), Box<dyn std::error::Error>> {
921902
// We can add shredding features as well as we are allowed to write unshredded variants
922903
// into shredded tables and shredded reads are explicitly blocked in the default
923904
// engine's parquet reader.
924-
// TODO: (#1124) we don't actually support column mapping writes yet, but have some
925-
// tests that do column mapping on writes. For now omit the writer feature to let tests
926-
// run, but after actual support this should be enabled.
927905
let table_url = create_table(
928906
store.clone(),
929907
table_location,
930908
table_schema.clone(),
931909
&[],
932910
true,
933-
vec!["variantType", "variantShredding-preview", "columnMapping"],
911+
vec!["variantType", "variantShredding-preview"],
934912
vec!["variantType", "variantShredding-preview"],
935913
)
936914
.await?;
@@ -1016,7 +994,7 @@ async fn test_append_variant() -> Result<(), Box<dyn std::error::Error>> {
1016994
Arc::new(Int32Array::from(i_values.clone())),
1017995
// nested struct<nested_v variant>
1018996
Arc::new(StructArray::try_new(
1019-
vec![Field::new("col21", variant_arrow_type_flipped(), true)].into(),
997+
vec![Field::new("nested_v", variant_arrow_type_flipped(), true)].into(),
1020998
vec![variant_nested_v_array.clone()],
1021999
None,
10221000
)?),

0 commit comments

Comments
 (0)