diff --git a/bindings/python/src/schema.rs b/bindings/python/src/schema.rs index 194cb3adc..3783e3a14 100644 --- a/bindings/python/src/schema.rs +++ b/bindings/python/src/schema.rs @@ -77,9 +77,9 @@ impl PyDataField { } fn field_type(&self) -> String { - // TODO(#284 follow-up): mirror Java DataType.asSQLString() once - // a Display impl is added to paimon::spec::DataType. - format!("{:?}", self.inner.data_type()) + // Java `DataField.type().asSQLString()`, which is what + // `Display for DataType` renders. + self.inner.data_type().to_string() } fn is_nullable(&self) -> bool { diff --git a/crates/paimon/src/spec/types.rs b/crates/paimon/src/spec/types.rs index 46d664dbd..2e56f9707 100644 --- a/crates/paimon/src/spec/types.rs +++ b/crates/paimon/src/spec/types.rs @@ -242,6 +242,18 @@ pub struct ArrayType { element_type: Box, } +impl Display for ArrayType { + /// Java `ArrayType.FORMAT` is `ARRAY<%s>`, filled with the element's own SQL + /// string so the element's `NOT NULL` lands inside the angle brackets. + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "ARRAY<{}>", self.element_type)?; + if !self.nullable { + write!(f, " NOT NULL")?; + } + Ok(()) + } +} + impl ArrayType { pub fn new(element_type: DataType) -> Self { Self::with_nullable(true, element_type) @@ -341,33 +353,13 @@ impl VectorType { pub fn length(&self) -> u32 { self.length } - - /// SQL name of a valid vector element type. - fn element_sql_name(&self) -> &'static str { - match self.element_type.as_ref() { - DataType::Boolean(_) => "BOOLEAN", - DataType::TinyInt(_) => "TINYINT", - DataType::SmallInt(_) => "SMALLINT", - DataType::Int(_) => "INT", - DataType::BigInt(_) => "BIGINT", - DataType::Float(_) => "FLOAT", - DataType::Double(_) => "DOUBLE", - other => { - unreachable!("vector element type validated at construction: {other:?}") - } - } - } } impl Display for VectorType { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { // Match Java VectorType.asSQLString: the element is rendered with its own // SQL string, which includes the element's nullability suffix. - write!(f, "VECTOR<{}", self.element_sql_name())?; - if !self.element_type.is_nullable() { - write!(f, " NOT NULL")?; - } - write!(f, ", {}>", self.length)?; + write!(f, "VECTOR<{}, {}>", self.element_type, self.length)?; if !self.nullable { write!(f, " NOT NULL")?; } @@ -472,6 +464,16 @@ pub struct BigIntType { nullable: bool, } +impl Display for BigIntType { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "BIGINT")?; + if !self.nullable { + write!(f, " NOT NULL")?; + } + Ok(()) + } +} + impl Default for BigIntType { fn default() -> Self { Self::new() @@ -592,6 +594,16 @@ pub struct BooleanType { nullable: bool, } +impl Display for BooleanType { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "BOOLEAN")?; + if !self.nullable { + write!(f, " NOT NULL")?; + } + Ok(()) + } +} + impl Default for BooleanType { fn default() -> Self { Self::new() @@ -625,6 +637,16 @@ pub struct BlobType { nullable: bool, } +impl Display for BlobType { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "BLOB")?; + if !self.nullable { + write!(f, " NOT NULL")?; + } + Ok(()) + } +} + impl Default for BlobType { fn default() -> Self { Self::new() @@ -659,6 +681,16 @@ pub struct VariantType { nullable: bool, } +impl Display for VariantType { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "VARIANT")?; + if !self.nullable { + write!(f, " NOT NULL")?; + } + Ok(()) + } +} + impl Default for VariantType { fn default() -> Self { Self::new() @@ -780,6 +812,16 @@ pub struct DateType { nullable: bool, } +impl Display for DateType { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "DATE")?; + if !self.nullable { + write!(f, " NOT NULL")?; + } + Ok(()) + } +} + impl Default for DateType { fn default() -> Self { Self::new() @@ -1001,6 +1043,16 @@ pub struct FloatType { nullable: bool, } +impl Display for FloatType { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "FLOAT")?; + if !self.nullable { + write!(f, " NOT NULL")?; + } + Ok(()) + } +} + impl Default for FloatType { fn default() -> Self { Self::new() @@ -1034,6 +1086,16 @@ pub struct IntType { nullable: bool, } +impl Display for IntType { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "INT")?; + if !self.nullable { + write!(f, " NOT NULL")?; + } + Ok(()) + } +} + impl Default for IntType { fn default() -> Self { Self::new() @@ -1173,6 +1235,16 @@ pub struct SmallIntType { nullable: bool, } +impl Display for SmallIntType { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "SMALLINT")?; + if !self.nullable { + write!(f, " NOT NULL")?; + } + Ok(()) + } +} + impl Default for SmallIntType { fn default() -> Self { Self::new() @@ -1404,6 +1476,16 @@ pub struct TinyIntType { nullable: bool, } +impl Display for TinyIntType { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "TINYINT")?; + if !self.nullable { + write!(f, " NOT NULL")?; + } + Ok(()) + } +} + impl Default for TinyIntType { fn default() -> Self { Self::new() @@ -1654,6 +1736,17 @@ pub struct MapType { value_type: Box, } +impl Display for MapType { + /// Java `MapType.FORMAT` is `MAP<%s, %s>`. + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "MAP<{}, {}>", self.key_type, self.value_type)?; + if !self.nullable { + write!(f, " NOT NULL")?; + } + Ok(()) + } +} + impl MapType { pub fn new(key_type: DataType, value_type: DataType) -> Self { Self::with_nullable(true, key_type, value_type) @@ -1696,6 +1789,17 @@ pub struct MultisetType { element_type: Box, } +impl Display for MultisetType { + /// Java `MultisetType.FORMAT` is `MULTISET<%s>`. + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "MULTISET<{}>", self.element_type)?; + if !self.nullable { + write!(f, " NOT NULL")?; + } + Ok(()) + } +} + impl MultisetType { pub fn new(element_type: DataType) -> Self { Self::with_nullable(true, element_type) @@ -1734,6 +1838,79 @@ pub struct RowType { fields: Vec, } +/// Render one row field the way Java `DataField.asSQLString` does: the escaped +/// name, a space, the field type's SQL string, then `COMMENT '...'` when the +/// field carries a description. Java also appends `DEFAULT `, which has no +/// counterpart on this struct. +fn write_row_field(f: &mut Formatter<'_>, field: &DataField) -> std::fmt::Result { + write!( + f, + "{} {}", + crate::spec::escape_identifier(field.name()), + field.data_type() + )?; + if let Some(description) = field.description().filter(|text| !text.is_empty()) { + write!( + f, + " COMMENT '{}'", + crate::spec::escape_single_quotes(description) + )?; + } + Ok(()) +} + +impl Display for RowType { + /// Java `RowType.FORMAT` is `ROW<%s>`, filled with the fields' own SQL strings + /// joined by `", "`. + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "ROW<")?; + for (index, field) in self.fields.iter().enumerate() { + if index > 0 { + write!(f, ", ")?; + } + write_row_field(f, field)?; + } + write!(f, ">")?; + if !self.nullable { + write!(f, " NOT NULL")?; + } + Ok(()) + } +} + +impl Display for DataType { + /// Dispatch to the variant's own SQL string, mirroring Java's abstract + /// `DataType.asSQLString`. Every arm is spelled out so a new variant fails to + /// compile rather than falling into a catch-all that renders the wrong text. + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + DataType::Boolean(t) => Display::fmt(t, f), + DataType::TinyInt(t) => Display::fmt(t, f), + DataType::SmallInt(t) => Display::fmt(t, f), + DataType::Int(t) => Display::fmt(t, f), + DataType::BigInt(t) => Display::fmt(t, f), + DataType::Decimal(t) => Display::fmt(t, f), + DataType::Double(t) => Display::fmt(t, f), + DataType::Float(t) => Display::fmt(t, f), + DataType::Binary(t) => Display::fmt(t, f), + DataType::VarBinary(t) => Display::fmt(t, f), + DataType::Variant(t) => Display::fmt(t, f), + DataType::Blob(t) => Display::fmt(t, f), + DataType::Char(t) => Display::fmt(t, f), + DataType::VarChar(t) => Display::fmt(t, f), + DataType::Date(t) => Display::fmt(t, f), + DataType::LocalZonedTimestamp(t) => Display::fmt(t, f), + DataType::Time(t) => Display::fmt(t, f), + DataType::Timestamp(t) => Display::fmt(t, f), + DataType::Array(t) => Display::fmt(t, f), + DataType::Map(t) => Display::fmt(t, f), + DataType::Multiset(t) => Display::fmt(t, f), + DataType::Row(t) => Display::fmt(t, f), + DataType::Vector(t) => Display::fmt(t, f), + } + } +} + impl RowType { pub const fn new(fields: Vec) -> Self { Self::with_nullable(true, fields) @@ -2620,6 +2797,116 @@ mod tests { )); } + /// Every variant must render its Java `asSQLString`. Spelled out one by one so + /// a wrong keyword cannot hide behind another arm. + #[test] + fn test_data_type_display_matches_java_sql_string() { + let cases: Vec<(DataType, &str)> = vec![ + (DataType::Boolean(BooleanType::new()), "BOOLEAN"), + (DataType::TinyInt(TinyIntType::new()), "TINYINT"), + (DataType::SmallInt(SmallIntType::new()), "SMALLINT"), + (DataType::Int(IntType::new()), "INT"), + (DataType::BigInt(BigIntType::new()), "BIGINT"), + (DataType::Float(FloatType::new()), "FLOAT"), + (DataType::Double(DoubleType::new()), "DOUBLE"), + (DataType::Date(DateType::new()), "DATE"), + (DataType::Variant(VariantType::new()), "VARIANT"), + (DataType::Blob(BlobType::new()), "BLOB"), + (DataType::Binary(BinaryType::new(1).unwrap()), "BINARY(1)"), + ( + DataType::Decimal(DecimalType::new(10, 2).unwrap()), + "DECIMAL(10, 2)", + ), + ( + DataType::VarChar(VarCharType::new(10).unwrap()), + "VARCHAR(10)", + ), + ]; + for (data_type, expected) in cases { + assert_eq!(data_type.to_string(), expected); + assert_eq!( + data_type.copy_with_nullable(false).unwrap().to_string(), + format!("{expected} NOT NULL"), + "NOT NULL suffix for {expected}" + ); + } + } + + /// Java renders a constructed type by filling its `FORMAT` with the children's + /// own SQL strings, so a child's `NOT NULL` sits inside the brackets while the + /// outer type's sits after them. + #[test] + fn test_constructed_data_type_display_nests_children() { + let int = DataType::Int(IntType::new()); + let not_null_int = int.copy_with_nullable(false).unwrap(); + + assert_eq!( + DataType::Array(ArrayType::new(int.clone())).to_string(), + "ARRAY" + ); + assert_eq!( + DataType::Array(ArrayType::with_nullable(false, not_null_int.clone())).to_string(), + "ARRAY NOT NULL" + ); + assert_eq!( + DataType::Multiset(MultisetType::new(int.clone())).to_string(), + "MULTISET" + ); + assert_eq!( + DataType::Map(MapType::new( + DataType::VarChar(VarCharType::new(8).unwrap()), + int.clone() + )) + .to_string(), + "MAP" + ); + assert_eq!( + DataType::Array(ArrayType::new(DataType::Map(MapType::new( + int.clone(), + DataType::Array(ArrayType::new(int.clone())) + )))) + .to_string(), + "ARRAY>>" + ); + } + + /// Java `RowType.FORMAT` is `ROW<%s>` over `DataField.asSQLString`, which is the + /// escaped name, the type, then `COMMENT '...'` when a description is present. + #[test] + fn test_row_type_display_renders_fields_like_java() { + let row = RowType::new(vec![ + DataField::new(0, "id".to_string(), DataType::Int(IntType::new())), + DataField::new( + 1, + "name".to_string(), + DataType::VarChar(VarCharType::new(10).unwrap()), + ), + ]); + assert_eq!( + DataType::Row(row).to_string(), + "ROW" + ); + + let described = RowType::with_nullable( + false, + vec![ + DataField::new(0, "id".to_string(), DataType::Int(IntType::new())) + .with_description(Some("the 'key'".to_string())), + ], + ); + assert_eq!( + DataType::Row(described).to_string(), + "ROW NOT NULL" + ); + + let quoted = RowType::new(vec![DataField::new( + 0, + "we\"ird".to_string(), + DataType::Int(IntType::new()), + )]); + assert_eq!(DataType::Row(quoted).to_string(), "ROW"); + } + #[test] fn test_vector_type_display() { let v = VectorType::try_new(true, 128, DataType::Float(FloatType::new())).unwrap();