This is an automated email from the ASF dual-hosted git repository.

JingsongLi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-rust.git


The following commit(s) were added to refs/heads/main by this push:
     new b567b6e6 fix(python): render field types as SQL strings, not Rust 
Debug (#786)
b567b6e6 is described below

commit b567b6e6d916078e49f17b55b50c73a78013a307
Author: jackylee <[email protected]>
AuthorDate: Fri Sep 18 14:47:23 2026 +0800

    fix(python): render field types as SQL strings, not Rust Debug (#786)
---
 bindings/python/src/schema.rs    |   6 +-
 crates/paimon/src/spec/schema.rs |  13 +-
 crates/paimon/src/spec/types.rs  | 347 ++++++++++++++++++++++++++++++++++++---
 3 files changed, 339 insertions(+), 27 deletions(-)

diff --git a/bindings/python/src/schema.rs b/bindings/python/src/schema.rs
index 194cb3ad..3783e3a1 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/schema.rs b/crates/paimon/src/spec/schema.rs
index 1e020eff..662889ad 100644
--- a/crates/paimon/src/spec/schema.rs
+++ b/crates/paimon/src/spec/schema.rs
@@ -1119,8 +1119,10 @@ impl DataField {
     }
 }
 
+/// Quote an identifier the way Java `EncodingUtils.escapeIdentifier` does: 
wrap it
+/// in backticks and double any backtick it contains.
 pub fn escape_identifier(identifier: &str) -> String {
-    identifier.replace('"', "\"\"")
+    format!("`{}`", identifier.replace('`', "``"))
 }
 
 pub fn escape_single_quotes(text: &str) -> String {
@@ -2386,8 +2388,13 @@ mod tests {
 
     #[test]
     fn test_escape_identifier() {
-        let escaped_identifier = escape_identifier("\"identifier\"");
-        assert_eq!(escaped_identifier, "\"\"identifier\"\"");
+        // Java doubles backticks and wraps in backticks; a double quote is not
+        // special (`EncodingUtils.escapeIdentifier`).
+        assert_eq!(escape_identifier("id"), "`id`");
+        assert_eq!(escape_identifier("a`b"), "`a``b`");
+        assert_eq!(escape_identifier("\"identifier\""), "`\"identifier\"`");
+        assert_eq!(escape_identifier("two words"), "`two words`");
+        assert_eq!(escape_identifier(""), "``");
     }
 
     #[test]
diff --git a/crates/paimon/src/spec/types.rs b/crates/paimon/src/spec/types.rs
index f14fc1cc..551dd08e 100644
--- a/crates/paimon/src/spec/types.rs
+++ b/crates/paimon/src/spec/types.rs
@@ -370,6 +370,18 @@ pub struct ArrayType {
     element_type: Box<DataType>,
 }
 
+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)
@@ -469,33 +481,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")?;
         }
@@ -600,6 +592,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()
@@ -720,6 +722,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()
@@ -753,6 +765,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()
@@ -787,6 +809,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()
@@ -908,6 +940,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()
@@ -1129,6 +1171,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()
@@ -1162,6 +1214,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()
@@ -1304,6 +1366,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()
@@ -1538,6 +1610,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()
@@ -1788,6 +1870,17 @@ pub struct MapType {
     value_type: Box<DataType>,
 }
 
+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)
@@ -1830,6 +1923,17 @@ pub struct MultisetType {
     element_type: Box<DataType>,
 }
 
+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)
@@ -1868,6 +1972,79 @@ pub struct RowType {
     fields: Vec<DataField>,
 }
 
+/// 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 <value>`, 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<DataField>) -> Self {
         Self::with_nullable(true, fields)
@@ -2783,6 +2960,134 @@ 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<INT>"
+        );
+        assert_eq!(
+            DataType::Array(ArrayType::with_nullable(false, 
not_null_int.clone())).to_string(),
+            "ARRAY<INT NOT NULL> NOT NULL"
+        );
+        assert_eq!(
+            DataType::Multiset(MultisetType::new(int.clone())).to_string(),
+            "MULTISET<INT>"
+        );
+        assert_eq!(
+            DataType::Map(MapType::new(
+                DataType::VarChar(VarCharType::new(8).unwrap()),
+                int.clone()
+            ))
+            .to_string(),
+            "MAP<VARCHAR(8), INT>"
+        );
+        assert_eq!(
+            DataType::Array(ArrayType::new(DataType::Map(MapType::new(
+                int.clone(),
+                DataType::Array(ArrayType::new(int.clone()))
+            ))))
+            .to_string(),
+            "ARRAY<MAP<INT, ARRAY<INT>>>"
+        );
+    }
+
+    /// 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<`id` INT, `name` VARCHAR(10)>"
+        );
+
+        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<`id` INT COMMENT 'the ''key'''> NOT NULL"
+        );
+
+        // A double quote is not special to backtick quoting; a backtick is 
doubled;
+        // a name with a space or a reserved word stays unambiguous.
+        let quoted = RowType::new(vec![DataField::new(
+            0,
+            "we\"ird".to_string(),
+            DataType::Int(IntType::new()),
+        )]);
+        assert_eq!(DataType::Row(quoted).to_string(), "ROW<`we\"ird` INT>");
+
+        let backticked = RowType::new(vec![DataField::new(
+            0,
+            "a`b".to_string(),
+            DataType::Int(IntType::new()),
+        )]);
+        assert_eq!(DataType::Row(backticked).to_string(), "ROW<`a``b` INT>");
+
+        let spaced = RowType::new(vec![
+            DataField::new(0, "two words".to_string(), 
DataType::Int(IntType::new())),
+            DataField::new(1, "select".to_string(), 
DataType::Int(IntType::new())),
+        ]);
+        assert_eq!(
+            DataType::Row(spaced).to_string(),
+            "ROW<`two words` INT, `select` INT>"
+        );
+    }
+
     #[test]
     fn test_vector_type_display() {
         let v = VectorType::try_new(true, 128, 
DataType::Float(FloatType::new())).unwrap();

Reply via email to