use tracing::warn;
use super::{
check_no_legacy_nested_ids, check_only_supported_types, IcebergCompatCheck,
IcebergCompatValidator, IcebergCompatVersion,
};
use crate::schema::PrimitiveType::*;
use crate::schema::{try_collect_column_defaults, DataType};
use crate::table_configuration::TableConfiguration;
use crate::DeltaResult;
pub(crate) const V3_VALIDATOR: IcebergCompatValidator = IcebergCompatValidator {
version: IcebergCompatVersion::V3,
checks: V3_CHECKS,
};
const V3_CHECKS: &[IcebergCompatCheck] = &[check_v3_supported_types, check_no_legacy_nested_ids];
fn is_v3_supported_type(dt: &DataType) -> bool {
matches!(
dt,
DataType::Primitive(
Byte | Short
| Integer
| Long
| Float
| Double
| Boolean
| Binary
| String
| Date
| Timestamp
| TimestampNtz
| Decimal(_)
) | DataType::Array(_)
| DataType::Map(_)
| DataType::Struct(_)
| DataType::Variant(_)
)
}
fn check_v3_supported_types(tc: &TableConfiguration) -> DeltaResult<()> {
check_only_supported_types(
tc,
is_v3_supported_type,
IcebergCompatVersion::V3.as_table_feature().as_ref(),
)
}
pub(crate) fn iceberg_compat_v3_column_defaults_validation(
table_configuration: &TableConfiguration,
) -> DeltaResult<()> {
for (path, column_default) in
try_collect_column_defaults(table_configuration.logical_schema_ref())?
{
if !column_default.is_kernel_parsable_literal() {
warn!(
"kernel could not verify that the icebergCompatV3 column default for '{path}' is a \
literal, got: {}",
column_default.raw_sql()
);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::schema::{schema, ArrayType, MapType};
#[test]
fn is_v3_supported_type_accepted_datatypes() {
let primitives = [
DataType::STRING,
DataType::LONG,
DataType::INTEGER,
DataType::SHORT,
DataType::BYTE,
DataType::FLOAT,
DataType::DOUBLE,
DataType::BOOLEAN,
DataType::BINARY,
DataType::DATE,
DataType::TIMESTAMP,
DataType::TIMESTAMP_NTZ,
DataType::decimal(10, 2).unwrap(),
];
for dt in primitives {
assert!(
is_v3_supported_type(&dt),
"primitive {dt} should be V3-supported"
);
}
let nested = [
DataType::from(ArrayType::new(DataType::INTEGER, true)),
DataType::from(MapType::new(DataType::STRING, DataType::INTEGER, true)),
DataType::from(schema! { nullable "x": INTEGER }),
DataType::unshredded_variant(),
];
for dt in nested {
assert!(
is_v3_supported_type(&dt),
"nested {dt} should be V3-supported"
);
}
}
#[test]
fn is_v3_supported_type_rejects_void() {
assert!(!is_v3_supported_type(&DataType::VOID));
}
}
#[cfg(test)]
mod column_default_tests {
use std::collections::HashMap;
use rstest::rstest;
use test_utils::LoggingTest;
use url::Url;
use super::iceberg_compat_v3_column_defaults_validation;
use crate::actions::{Metadata, Protocol};
use crate::schema::ColumnMetadataKey::CurrentDefault;
use crate::schema::{ArrayType, DataType, MetadataValue, StructField, StructType};
use crate::table_configuration::TableConfiguration;
use crate::table_features::TableFeature;
fn table_config_with_schema(schema: StructType) -> TableConfiguration {
let metadata = Metadata::try_new(
None,
None,
std::sync::Arc::new(schema),
vec![],
0,
HashMap::new(),
)
.unwrap();
let protocol = Protocol::try_new_modern(
TableFeature::EMPTY_LIST,
[TableFeature::AllowColumnDefaults],
)
.unwrap();
TableConfiguration::try_new(metadata, protocol, Url::parse("file:///t/").unwrap(), 0)
.unwrap()
}
fn field_with_default(
name: &str,
data_type: impl Into<DataType>,
default_sql: &str,
) -> StructField {
StructField::nullable(name, data_type).add_metadata([(
CurrentDefault.as_ref().to_string(),
MetadataValue::String(default_sql.to_string()),
)])
}
#[rstest]
#[case::primitive_literal(
StructType::try_new([field_with_default("a", DataType::INTEGER, "42")]).unwrap(),
"a",
None
)]
#[case::primitive_null(
StructType::try_new([field_with_default("a", DataType::INTEGER, "NULL")]).unwrap(),
"a",
None
)]
#[case::non_literal_primitive(
StructType::try_new([field_with_default(
"a",
DataType::TIMESTAMP,
"current_timestamp()"
)]).unwrap(),
"a",
Some("could not verify")
)]
#[case::null_on_non_primitive(
StructType::try_new([field_with_default(
"a",
ArrayType::new(DataType::INTEGER, true),
"NULL"
)]).unwrap(),
"a",
None
)]
#[case::non_null_on_non_primitive(
StructType::try_new([field_with_default(
"a",
ArrayType::new(DataType::INTEGER, true),
"ARRAY(1)"
)]).unwrap(),
"a",
Some("could not verify")
)]
#[case::nested_non_literal(
StructType::try_new([StructField::nullable(
"s",
DataType::try_struct_type([field_with_default(
"inner",
DataType::TIMESTAMP,
"current_timestamp()"
)]).unwrap(),
)]).unwrap(),
"s.inner",
Some("could not verify")
)]
fn v3_column_default_validation(
#[case] schema: StructType,
#[case] expected_path: &str,
#[case] expected_warning: Option<&str>,
) {
let table_configuration = table_config_with_schema(schema);
let logging = LoggingTest::new();
iceberg_compat_v3_column_defaults_validation(&table_configuration).unwrap();
let logs = logging.logs();
match expected_warning {
None => assert!(
!logs.contains("icebergCompatV3 column default"),
"logs: {logs}"
),
Some(needle) => {
assert!(logs.contains(expected_path), "logs: {logs}");
assert!(logs.contains(needle), "logs: {logs}");
}
}
}
}