Skip to content

Commit cacfd33

Browse files
committed
Add validation for timestampNtz
1 parent ebac47a commit cacfd33

3 files changed

Lines changed: 191 additions & 2 deletions

File tree

kernel/src/table_configuration.rs

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ use url::Url;
1515
use crate::actions::{ensure_supported_features, Metadata, Protocol};
1616
use crate::schema::{InvariantChecker, SchemaRef};
1717
use crate::table_features::{
18-
column_mapping_mode, validate_schema_column_mapping, ColumnMappingMode, ReaderFeature,
19-
WriterFeature,
18+
column_mapping_mode, validate_schema_column_mapping, validate_timestamp_ntz_feature_support,
19+
ColumnMappingMode, ReaderFeature, WriterFeature,
2020
};
2121
use crate::table_properties::TableProperties;
2222
use crate::{DeltaResult, Error, Version};
@@ -78,6 +78,9 @@ impl TableConfiguration {
7878

7979
// validate column mapping mode -- all schema fields should be correctly (un)annotated
8080
validate_schema_column_mapping(&schema, column_mapping_mode)?;
81+
82+
validate_timestamp_ntz_feature_support(&schema, &protocol)?;
83+
8184
Ok(Self {
8285
schema,
8386
metadata,
@@ -574,4 +577,55 @@ mod test {
574577
);
575578
assert_eq!(new_table_config.table_root(), table_config.table_root());
576579
}
580+
581+
#[test]
582+
fn test_timestamp_ntz_validation_integration() {
583+
// Schema with TIMESTAMP_NTZ column
584+
let schema_string = r#"{"type":"struct","fields":[{"name":"ts","type":"timestamp_ntz","nullable":true,"metadata":{}}]}"#.to_string();
585+
let metadata = Metadata {
586+
schema_string,
587+
..Default::default()
588+
};
589+
590+
let protocol_without_timestamp_ntz_features = Protocol::try_new(
591+
3,
592+
7,
593+
Some::<Vec<String>>(vec![]),
594+
Some::<Vec<String>>(vec![]),
595+
)
596+
.unwrap();
597+
598+
let protocol_with_timestamp_ntz_features = Protocol::try_new(
599+
3,
600+
7,
601+
Some([ReaderFeature::TimestampWithoutTimezone]),
602+
Some([WriterFeature::TimestampWithoutTimezone]),
603+
)
604+
.unwrap();
605+
606+
let table_root = Url::try_from("file:///").unwrap();
607+
608+
let result = TableConfiguration::try_new(
609+
metadata.clone(),
610+
protocol_without_timestamp_ntz_features,
611+
table_root.clone(),
612+
0,
613+
);
614+
assert!(
615+
result.is_err(),
616+
"Should fail when TIMESTAMP_NTZ is used without required features"
617+
);
618+
assert!(result.unwrap_err().to_string().contains("timestampNtz"));
619+
620+
let result = TableConfiguration::try_new(
621+
metadata,
622+
protocol_with_timestamp_ntz_features,
623+
table_root,
624+
0,
625+
);
626+
assert!(
627+
result.is_ok(),
628+
"Should succeed when TIMESTAMP_NTZ is used with required features"
629+
);
630+
}
577631
}

kernel/src/table_features/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ use crate::schema::DataType;
88

99
pub(crate) use column_mapping::column_mapping_mode;
1010
pub use column_mapping::{validate_schema_column_mapping, ColumnMappingMode};
11+
pub use timestamp_ntz::validate_timestamp_ntz_feature_support;
1112
mod column_mapping;
13+
mod timestamp_ntz;
1214

1315
/// Reader features communicate capabilities that must be implemented in order to correctly read a
1416
/// given table. That is, readers must implement and respect all features listed in a table's
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
//! Validation for TIMESTAMP_NTZ feature support
2+
3+
use super::{ReaderFeature, WriterFeature};
4+
use crate::actions::Protocol;
5+
use crate::schema::{PrimitiveType, Schema, SchemaTransform};
6+
use crate::utils::require;
7+
use crate::{DeltaResult, Error};
8+
9+
use std::borrow::Cow;
10+
11+
/// Validates that if a table schema contains TIMESTAMP_NTZ columns, the table must have the
12+
/// TimestampWithoutTimezone feature in both reader and writer features.
13+
pub fn validate_timestamp_ntz_feature_support(
14+
schema: &Schema,
15+
protocol: &Protocol,
16+
) -> DeltaResult<()> {
17+
if !protocol.has_reader_feature(&ReaderFeature::TimestampWithoutTimezone)
18+
|| !protocol.has_writer_feature(&WriterFeature::TimestampWithoutTimezone)
19+
{
20+
let mut uses_timestamp_ntz = UsesTimestampNtz(false);
21+
let _ = uses_timestamp_ntz.transform_struct(schema);
22+
require!(
23+
!uses_timestamp_ntz.0,
24+
Error::unsupported(
25+
"Table contains TIMESTAMP_NTZ columns but does not have the required 'timestampNtz' feature in reader and writer features"
26+
)
27+
);
28+
}
29+
Ok(())
30+
}
31+
32+
/// Schema visitor that checks if any column in the schema uses TIMESTAMP_NTZ type
33+
struct UsesTimestampNtz(bool);
34+
35+
impl<'a> SchemaTransform<'a> for UsesTimestampNtz {
36+
fn transform_primitive(&mut self, ptype: &'a PrimitiveType) -> Option<Cow<'a, PrimitiveType>> {
37+
if *ptype == PrimitiveType::TimestampNtz {
38+
self.0 = true;
39+
}
40+
None
41+
}
42+
}
43+
44+
#[cfg(test)]
45+
mod tests {
46+
use super::*;
47+
use crate::actions::Protocol;
48+
use crate::schema::{DataType, PrimitiveType, StructField, StructType};
49+
use crate::table_features::{ReaderFeature, WriterFeature};
50+
51+
#[test]
52+
fn test_timestamp_ntz_feature_validation() {
53+
let schema_with_timestamp_ntz = StructType::new([
54+
StructField::new("id", DataType::INTEGER, false),
55+
StructField::new("ts", DataType::Primitive(PrimitiveType::TimestampNtz), true),
56+
]);
57+
58+
let schema_without_timestamp_ntz = StructType::new([
59+
StructField::new("id", DataType::INTEGER, false),
60+
StructField::new("name", DataType::STRING, true),
61+
]);
62+
63+
// Protocol with TimestampWithoutTimezone features
64+
let protocol_with_features = Protocol::try_new(
65+
3,
66+
7,
67+
Some([ReaderFeature::TimestampWithoutTimezone]),
68+
Some([WriterFeature::TimestampWithoutTimezone]),
69+
)
70+
.unwrap();
71+
72+
// Protocol without TimestampWithoutTimezone features
73+
let protocol_without_features = Protocol::try_new(
74+
3,
75+
7,
76+
Some::<Vec<String>>(vec![]),
77+
Some::<Vec<String>>(vec![]),
78+
)
79+
.unwrap();
80+
81+
// Schema with TIMESTAMP_NTZ + Protocol with features = OK
82+
validate_timestamp_ntz_feature_support(&schema_with_timestamp_ntz, &protocol_with_features)
83+
.expect("Should succeed when features are present");
84+
85+
// Schema without TIMESTAMP_NTZ + Protocol without features = OK
86+
validate_timestamp_ntz_feature_support(
87+
&schema_without_timestamp_ntz,
88+
&protocol_without_features,
89+
)
90+
.expect("Should succeed when no TIMESTAMP_NTZ columns are present");
91+
92+
// Schema without TIMESTAMP_NTZ + Protocol with features = OK
93+
validate_timestamp_ntz_feature_support(
94+
&schema_without_timestamp_ntz,
95+
&protocol_with_features,
96+
)
97+
.expect("Should succeed when no TIMESTAMP_NTZ columns are present, even with features");
98+
99+
// Schema with TIMESTAMP_NTZ + Protocol without features = ERROR
100+
let result = validate_timestamp_ntz_feature_support(
101+
&schema_with_timestamp_ntz,
102+
&protocol_without_features,
103+
);
104+
assert!(
105+
result.is_err(),
106+
"Should fail when TIMESTAMP_NTZ columns are present but features are missing"
107+
);
108+
assert!(result.unwrap_err().to_string().contains("timestampNtz"));
109+
110+
// Nested schema with TIMESTAMP_NTZ
111+
let nested_schema_with_timestamp_ntz = StructType::new([
112+
StructField::new("id", DataType::INTEGER, false),
113+
StructField::new(
114+
"nested",
115+
DataType::Struct(Box::new(StructType::new([StructField::new(
116+
"inner_ts",
117+
DataType::Primitive(PrimitiveType::TimestampNtz),
118+
true,
119+
)]))),
120+
true,
121+
),
122+
]);
123+
124+
let result = validate_timestamp_ntz_feature_support(
125+
&nested_schema_with_timestamp_ntz,
126+
&protocol_without_features,
127+
);
128+
assert!(
129+
result.is_err(),
130+
"Should fail for nested TIMESTAMP_NTZ columns when features are missing"
131+
);
132+
}
133+
}

0 commit comments

Comments
 (0)