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 dee5ac51 perf(spec): deserialize DataType by keyword instead of 
untagged trial (#867)
dee5ac51 is described below

commit dee5ac518996ac45228c5a3fa867c7bca9699ba7
Author: Dapeng Sun(孙大鹏) <[email protected]>
AuthorDate: Fri Sep 18 14:15:17 2026 +0800

    perf(spec): deserialize DataType by keyword instead of untagged trial (#867)
---
 crates/paimon/src/spec/types.rs | 228 +++++++++++++++++++++++++++++++++++++++-
 1 file changed, 227 insertions(+), 1 deletion(-)

diff --git a/crates/paimon/src/spec/types.rs b/crates/paimon/src/spec/types.rs
index e159d6e8..f14fc1cc 100644
--- a/crates/paimon/src/spec/types.rs
+++ b/crates/paimon/src/spec/types.rs
@@ -51,7 +51,7 @@ bitflags! {
 /// Data type for paimon table.
 ///
 /// Impl Reference: 
<https://github.com/apache/paimon/blob/release-0.8.2/paimon-common/src/main/java/org/apache/paimon/types/DataType.java#L45>
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
 #[serde(untagged)]
 pub enum DataType {
     /// Data type of a boolean with a (possibly) three-valued logic of `TRUE`, 
`FALSE`, `UNKNOWN`.
@@ -109,7 +109,119 @@ pub enum DataType {
     Vector(VectorType),
 }
 
+/// Names of the constructed types that are encoded as JSON objects.
+const CONSTRUCTED_TYPE_NAMES: &[&str] = &["ARRAY", "MAP", "MULTISET", "ROW", 
"VECTOR"];
+
+/// The type keyword before any length, precision or nullability suffix.
+fn type_base_name(s: &str) -> &str {
+    s.split([' ', '(']).next().unwrap_or(s)
+}
+
+/// Nullability of a plain keyword type (`NAME` or `NAME NOT NULL`), `None` 
when `s` is neither.
+fn plain_type_nullable(s: &str, name: &str) -> Option<bool> {
+    let (base, rest) = s.split_once(' ').unwrap_or((s, ""));
+    if base != name {
+        None
+    } else if rest.is_empty() {
+        Some(true)
+    } else if rest.contains("NOT NULL") {
+        Some(false)
+    } else {
+        None
+    }
+}
+
+impl<'de> Deserialize<'de> for DataType {
+    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        deserializer.deserialize_any(DataTypeVisitor)
+    }
+}
+
+// Dispatches on the type name once; an untagged enum would try every variant 
in turn and
+// format an error for each rejected one, which dominated parsing of wide 
schemas.
+struct DataTypeVisitor;
+
+impl<'de> serde::de::Visitor<'de> for DataTypeVisitor {
+    type Value = DataType;
+
+    fn expecting(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
+        formatter.write_str("a Paimon data type string or a constructed type 
object")
+    }
+
+    fn visit_str<E>(self, value: &str) -> std::result::Result<DataType, E>
+    where
+        E: serde::de::Error,
+    {
+        DataType::parse_atomic(value).map_err(E::custom)
+    }
+
+    fn visit_map<A>(self, map: A) -> std::result::Result<DataType, A::Error>
+    where
+        A: serde::de::MapAccess<'de>,
+    {
+        let value =
+            
serde_json::Value::deserialize(serde::de::value::MapAccessDeserializer::new(map))?;
+        let name = value
+            .get("type")
+            .and_then(serde_json::Value::as_str)
+            .map(|name| type_base_name(name).to_string())
+            .ok_or_else(|| serde::de::Error::missing_field("type"))?;
+        let parsed = match name.as_str() {
+            "ARRAY" => ArrayType::deserialize(value).map(DataType::Array),
+            "MAP" => MapType::deserialize(value).map(DataType::Map),
+            "MULTISET" => 
MultisetType::deserialize(value).map(DataType::Multiset),
+            "ROW" => RowType::deserialize(value).map(DataType::Row),
+            "VECTOR" => VectorType::deserialize(value).map(DataType::Vector),
+            other => {
+                return Err(serde::de::Error::unknown_variant(
+                    other,
+                    CONSTRUCTED_TYPE_NAMES,
+                ))
+            }
+        };
+        parsed.map_err(serde::de::Error::custom)
+    }
+}
+
 impl DataType {
+    /// Parses an atomic type string such as `DOUBLE`, `VARCHAR(10) NOT NULL` 
or
+    /// `TIMESTAMP(6) WITH LOCAL TIME ZONE`, choosing the parser by the 
leading keyword.
+    fn parse_atomic(s: &str) -> Result<DataType> {
+        let invalid = || Error::DataTypeInvalid {
+            message: format!("Invalid data type: {s}"),
+        };
+        let plain = |name: &str| plain_type_nullable(s, 
name).ok_or_else(invalid);
+        let parsed = match type_base_name(s) {
+            "BOOLEAN" => 
DataType::Boolean(BooleanType::with_nullable(plain("BOOLEAN")?)),
+            "TINYINT" => 
DataType::TinyInt(TinyIntType::with_nullable(plain("TINYINT")?)),
+            "SMALLINT" => 
DataType::SmallInt(SmallIntType::with_nullable(plain("SMALLINT")?)),
+            "INT" => DataType::Int(IntType::with_nullable(plain("INT")?)),
+            "BIGINT" => 
DataType::BigInt(BigIntType::with_nullable(plain("BIGINT")?)),
+            "FLOAT" => 
DataType::Float(FloatType::with_nullable(plain("FLOAT")?)),
+            "DOUBLE" => 
DataType::Double(DoubleType::with_nullable(plain("DOUBLE")?)),
+            "DATE" => DataType::Date(DateType::with_nullable(plain("DATE")?)),
+            "VARIANT" => 
DataType::Variant(VariantType::with_nullable(plain("VARIANT")?)),
+            "BLOB" => DataType::Blob(BlobType::with_nullable(plain("BLOB")?)),
+            "DECIMAL" => DataType::Decimal(DecimalType::from_str(s)?),
+            "BINARY" => DataType::Binary(BinaryType::from_str(s)?),
+            "BYTES" | "VARBINARY" => 
DataType::VarBinary(VarBinaryType::from_str(s)?),
+            "CHAR" => DataType::Char(CharType::from_str(s)?),
+            "STRING" | "VARCHAR" => 
DataType::VarChar(VarCharType::from_str(s)?),
+            "TIME" => DataType::Time(TimeType::from_str(s)?),
+            // PyPaimon persists the TIMESTAMP_LTZ(p) alias for this type.
+            "TIMESTAMP_LTZ" => 
DataType::LocalZonedTimestamp(LocalZonedTimestampType::from_str(s)?),
+            "TIMESTAMP" if s.contains("WITH LOCAL TIME ZONE") => {
+                
DataType::LocalZonedTimestamp(LocalZonedTimestampType::from_str(s)?)
+            }
+            "TIMESTAMP" => DataType::Timestamp(TimestampType::from_str(s)?),
+            _ => return Err(invalid()),
+        };
+        Ok(parsed)
+    }
+
     /// Returns whether this type is or contains (recursively) a [`RowType`].
     /// Used to reject schema columns that would require field ID assignment 
for nested row fields,
     /// which is not yet implemented (see 
<https://github.com/apache/paimon/pull/1547>).
@@ -2773,4 +2885,118 @@ mod tests {
         let arrow = crate::arrow::paimon_type_to_arrow(&dt).unwrap();
         assert!(matches!(arrow, arrow_schema::DataType::FixedSizeList(_, 4)));
     }
+
+    #[test]
+    fn test_datatype_deserialize_atomic_by_keyword() {
+        for (json, nullable) in [
+            ("BOOLEAN", true),
+            ("BOOLEAN NOT NULL", false),
+            ("TINYINT", true),
+            ("SMALLINT NOT NULL", false),
+            ("INT", true),
+            ("BIGINT NOT NULL", false),
+            ("FLOAT", true),
+            ("DOUBLE NOT NULL", false),
+            ("DATE", true),
+            ("VARIANT NOT NULL", false),
+            ("BLOB", true),
+            ("DECIMAL(10, 2)", true),
+            ("DECIMAL(10, 2) NOT NULL", false),
+            ("BINARY(3)", true),
+            ("VARBINARY(3) NOT NULL", false),
+            ("BYTES", true),
+            ("CHAR(3)", true),
+            ("VARCHAR(3) NOT NULL", false),
+            ("STRING", true),
+            ("STRING NOT NULL", false),
+            ("TIME(3)", true),
+            ("TIMESTAMP(6)", true),
+            ("TIMESTAMP(6) NOT NULL", false),
+            ("TIMESTAMP(6) WITH LOCAL TIME ZONE", true),
+            ("TIMESTAMP(6) WITH LOCAL TIME ZONE NOT NULL", false),
+            ("TIMESTAMP_LTZ(3)", true),
+            ("TIMESTAMP_LTZ(3) NOT NULL", false),
+        ] {
+            let parsed: DataType = 
serde_json::from_str(&format!("\"{json}\"")).unwrap();
+            assert_eq!(parsed.is_nullable(), nullable, "{json}");
+            let roundtrip: DataType =
+                
serde_json::from_str(&serde_json::to_string(&parsed).unwrap()).unwrap();
+            assert_eq!(roundtrip, parsed, "{json}");
+        }
+        assert!(matches!(
+            serde_json::from_str::<DataType>("\"STRING\"").unwrap(),
+            DataType::VarChar(_)
+        ));
+        assert!(matches!(
+            serde_json::from_str::<DataType>("\"BYTES\"").unwrap(),
+            DataType::VarBinary(_)
+        ));
+        assert!(matches!(
+            serde_json::from_str::<DataType>("\"TIMESTAMP(3) WITH LOCAL TIME 
ZONE\"").unwrap(),
+            DataType::LocalZonedTimestamp(_)
+        ));
+        assert!(matches!(
+            serde_json::from_str::<DataType>("\"TIMESTAMP(3)\"").unwrap(),
+            DataType::Timestamp(_)
+        ));
+        assert!(matches!(
+            serde_json::from_str::<DataType>("\"TIME(3)\"").unwrap(),
+            DataType::Time(_)
+        ));
+    }
+
+    #[test]
+    fn test_datatype_deserialize_constructed_by_name() {
+        let array: DataType =
+            serde_json::from_str(r#"{"type":"ARRAY NOT 
NULL","element":"INT"}"#).unwrap();
+        assert_eq!(
+            array,
+            DataType::Array(ArrayType::with_nullable(
+                false,
+                DataType::Int(IntType::new())
+            ))
+        );
+        let map: DataType =
+            
serde_json::from_str(r#"{"type":"MAP","key":"STRING","value":"DOUBLE NOT 
NULL"}"#)
+                .unwrap();
+        assert!(matches!(map, DataType::Map(_)));
+        assert!(map.is_nullable());
+        let multiset: DataType =
+            
serde_json::from_str(r#"{"type":"MULTISET","element":"BIGINT"}"#).unwrap();
+        assert!(matches!(multiset, DataType::Multiset(_)));
+        let row: DataType = serde_json::from_str(
+            r#"{"type":"ROW NOT 
NULL","fields":[{"id":0,"name":"a","type":"INT"},{"id":1,"name":"b","type":{"type":"ARRAY","element":"STRING"}}]}"#,
+        )
+        .unwrap();
+        match &row {
+            DataType::Row(row_type) => {
+                assert_eq!(row_type.fields().len(), 2);
+                assert!(matches!(
+                    row_type.fields()[1].data_type(),
+                    DataType::Array(_)
+                ));
+            }
+            other => panic!("expected ROW, got {other:?}"),
+        }
+        assert!(!row.is_nullable());
+        // Key order must not matter for the dispatch on "type".
+        let vector: DataType =
+            
serde_json::from_str(r#"{"length":4,"element":"FLOAT","type":"VECTOR"}"#).unwrap();
+        assert_eq!(
+            vector,
+            DataType::Vector(
+                VectorType::try_new(true, 4, 
DataType::Float(FloatType::new())).unwrap()
+            )
+        );
+    }
+
+    #[test]
+    fn test_datatype_deserialize_rejects_unknown_shapes() {
+        assert!(serde_json::from_str::<DataType>("\"TUPLE\"").is_err());
+        assert!(serde_json::from_str::<DataType>("\"INT NULLABLE\"").is_err());
+        assert!(serde_json::from_str::<DataType>("\"VARCHAR\"").is_err());
+        
assert!(serde_json::from_str::<DataType>(r#"{"type":"TUPLE","element":"INT"}"#).is_err());
+        
assert!(serde_json::from_str::<DataType>(r#"{"element":"INT"}"#).is_err());
+        assert!(serde_json::from_str::<DataType>("7").is_err());
+    }
 }

Reply via email to