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 e2ea7ca [type] Add native VectorType to the type system (#410 PR 1)
(#411)
e2ea7ca is described below
commit e2ea7ca7d04c02bb23aadde2e8e331319590cbb1
Author: Junrui Lee <[email protected]>
AuthorDate: Sat Jun 27 22:38:26 2026 +0800
[type] Add native VectorType to the type system (#410 PR 1) (#411)
---
crates/paimon/src/arrow/format/parquet.rs | 3 +-
crates/paimon/src/arrow/format/vortex.rs | 6 +-
crates/paimon/src/arrow/mod.rs | 5 +
crates/paimon/src/spec/data_type_casts.rs | 3 +-
crates/paimon/src/spec/partition_utils.rs | 3 +-
crates/paimon/src/spec/schema.rs | 112 +++++++++-
crates/paimon/src/spec/types.rs | 354 ++++++++++++++++++++++++++++++
7 files changed, 481 insertions(+), 5 deletions(-)
diff --git a/crates/paimon/src/arrow/format/parquet.rs
b/crates/paimon/src/arrow/format/parquet.rs
index 0ff0a2c..bec9466 100644
--- a/crates/paimon/src/arrow/format/parquet.rs
+++ b/crates/paimon/src/arrow/format/parquet.rs
@@ -759,7 +759,8 @@ fn literal_scalar_for_parquet_filter(
| DataType::Array(_)
| DataType::Map(_)
| DataType::Multiset(_)
- | DataType::Row(_) => return Ok(None),
+ | DataType::Row(_)
+ | DataType::Vector(_) => return Ok(None),
};
Ok(Some(Scalar::new(array)))
diff --git a/crates/paimon/src/arrow/format/vortex.rs
b/crates/paimon/src/arrow/format/vortex.rs
index 88eb16e..1d093a3 100644
--- a/crates/paimon/src/arrow/format/vortex.rs
+++ b/crates/paimon/src/arrow/format/vortex.rs
@@ -745,7 +745,11 @@ fn literal_scalar_for_arrow_filter(
}
_ => return Ok(None),
},
- DataType::Array(_) | DataType::Map(_) | DataType::Multiset(_) |
DataType::Row(_) => {
+ DataType::Array(_)
+ | DataType::Map(_)
+ | DataType::Multiset(_)
+ | DataType::Row(_)
+ | DataType::Vector(_) => {
return Ok(None);
}
};
diff --git a/crates/paimon/src/arrow/mod.rs b/crates/paimon/src/arrow/mod.rs
index 50a3a68..fc6451f 100644
--- a/crates/paimon/src/arrow/mod.rs
+++ b/crates/paimon/src/arrow/mod.rs
@@ -117,6 +117,11 @@ pub fn paimon_type_to_arrow(dt: &PaimonDataType) ->
crate::Result<ArrowDataType>
.collect::<crate::Result<Vec<_>>>()?;
ArrowDataType::Struct(fields.into())
}
+ PaimonDataType::Vector(_) => {
+ return Err(crate::Error::Unsupported {
+ message: "VectorType is not yet supported in arrow
conversion".to_string(),
+ })
+ }
})
}
diff --git a/crates/paimon/src/spec/data_type_casts.rs
b/crates/paimon/src/spec/data_type_casts.rs
index 7f871cd..c44c4a4 100644
--- a/crates/paimon/src/spec/data_type_casts.rs
+++ b/crates/paimon/src/spec/data_type_casts.rs
@@ -165,7 +165,8 @@ fn explicit_cast_supported(source: &DataType, target:
&DataType) -> bool {
| DataType::Array(_)
| DataType::Map(_)
| DataType::Multiset(_)
- | DataType::Row(_) => false,
+ | DataType::Row(_)
+ | DataType::Vector(_) => false,
}
}
diff --git a/crates/paimon/src/spec/partition_utils.rs
b/crates/paimon/src/spec/partition_utils.rs
index 427ea35..db0f2c2 100644
--- a/crates/paimon/src/spec/partition_utils.rs
+++ b/crates/paimon/src/spec/partition_utils.rs
@@ -269,7 +269,8 @@ fn format_partition_value(
| DataType::Array(_)
| DataType::Map(_)
| DataType::Multiset(_)
- | DataType::Row(_) => {
+ | DataType::Row(_)
+ | DataType::Vector(_) => {
return Err(Error::Unsupported {
message: format!("{data_type:?} type is not supported as
partition key"),
});
diff --git a/crates/paimon/src/spec/schema.rs b/crates/paimon/src/spec/schema.rs
index 812a9b9..777b62f 100644
--- a/crates/paimon/src/spec/schema.rs
+++ b/crates/paimon/src/spec/schema.rs
@@ -434,6 +434,11 @@ impl TableSchema {
// Re-run create-time validations on the final schema, mirroring Java
// `SchemaValidation.validateTableSchema` after applying changes.
+ Schema::validate_key_field_types(
+ &new_schema.fields,
+ &new_schema.primary_keys,
+ &new_schema.options,
+ )?;
Schema::validate_blob_fields(
&new_schema.fields,
&new_schema.partition_keys,
@@ -785,6 +790,7 @@ impl Schema {
let primary_keys = Self::normalize_primary_keys(&primary_keys, &mut
options)?;
let partition_keys = Self::normalize_partition_keys(&partition_keys,
&mut options)?;
let fields = Self::normalize_fields(&fields, &partition_keys,
&primary_keys)?;
+ Self::validate_key_field_types(&fields, &primary_keys, &options)?;
Self::validate_blob_fields(&fields, &partition_keys, &options)?;
PartialUpdateConfig::new(&options).validate_create_mode(!primary_keys.is_empty())?;
AggregationConfig::new(&options).validate_create_mode(&primary_keys,
&fields)?;
@@ -967,6 +973,39 @@ impl Schema {
Ok(())
}
+ /// Reject types that cannot serve as a key (primary key or explicit
+ /// `bucket-key`). Currently only `VECTOR` is rejected here: it is densely
+ /// stored and has no key ordering, so it cannot be used as a key column.
+ fn validate_key_field_types(
+ fields: &[DataField],
+ primary_keys: &[String],
+ options: &HashMap<String, String>,
+ ) -> crate::Result<()> {
+ let reject = |key_kind: &str, name: &str| -> crate::Result<()> {
+ let field = fields.iter().find(|f| f.name() == name);
+ if let Some(field) = field {
+ if matches!(field.data_type(), DataType::Vector(_)) {
+ return Err(crate::Error::ConfigInvalid {
+ message: format!(
+ "The VECTOR type of {key_kind} field '{name}' is
unsupported."
+ ),
+ });
+ }
+ }
+ Ok(())
+ };
+
+ for pk in primary_keys {
+ reject("primary key", pk)?;
+ }
+ if let Some(bucket_keys) = CoreOptions::new(options).bucket_key() {
+ for bk in &bucket_keys {
+ reject("bucket key", bk)?;
+ }
+ }
+ Ok(())
+ }
+
fn validate_blob_fields(
fields: &[DataField],
partition_keys: &[String],
@@ -1246,7 +1285,7 @@ impl Default for SchemaBuilder {
#[cfg(test)]
mod tests {
- use crate::spec::{BlobType, IntType, VarCharType};
+ use crate::spec::{BlobType, FloatType, IntType, VarCharType, VectorType};
use super::*;
@@ -1977,6 +2016,77 @@ mod tests {
assert_eq!(new_schema.fields().len(), 2);
}
+ fn vector_4f() -> DataType {
+ DataType::Vector(VectorType::try_new(true, 4,
DataType::Float(FloatType::new())).unwrap())
+ }
+
+ #[test]
+ fn test_vector_rejected_as_primary_key() {
+ let err = Schema::builder()
+ .column("id", vector_4f())
+ .column("name", DataType::Int(IntType::new()))
+ .primary_key(["id"])
+ .build()
+ .unwrap_err();
+ assert!(
+ matches!(err, crate::Error::ConfigInvalid { ref message }
+ if message.contains("primary key") &&
message.contains("VECTOR")),
+ "VECTOR primary key should be rejected, got {err:?}"
+ );
+ }
+
+ #[test]
+ fn test_vector_rejected_as_explicit_bucket_key() {
+ let err = Schema::builder()
+ .column("id", DataType::Int(IntType::new()))
+ .column("embedding", vector_4f())
+ .option(BUCKET_KEY_OPTION, "embedding")
+ .build()
+ .unwrap_err();
+ assert!(
+ matches!(err, crate::Error::ConfigInvalid { ref message }
+ if message.contains("bucket key") &&
message.contains("VECTOR")),
+ "VECTOR explicit bucket key should be rejected, got {err:?}"
+ );
+ }
+
+ #[test]
+ fn test_vector_allowed_as_non_key_column() {
+ // A VECTOR column that is not a key (no pk, no explicit bucket-key)
must
+ // build fine — the implicit "all non-partition fields" bucket-key
fallback
+ // must NOT reject it.
+ let schema = Schema::builder()
+ .column("id", DataType::Int(IntType::new()))
+ .column("embedding", vector_4f())
+ .build()
+ .unwrap();
+ assert_eq!(schema.fields().len(), 2);
+ }
+
+ #[test]
+ fn test_apply_changes_revalidates_vector_bucket_key() {
+ let table_schema = TableSchema::new(
+ 0,
+ &Schema::builder()
+ .column("id", DataType::Int(IntType::new()))
+ .column("embedding", vector_4f())
+ .build()
+ .unwrap(),
+ );
+
+ let err = table_schema
+ .apply_changes(vec![crate::spec::SchemaChange::set_option(
+ BUCKET_KEY_OPTION.to_string(),
+ "embedding".to_string(),
+ )])
+ .unwrap_err();
+ assert!(
+ matches!(err, crate::Error::ConfigInvalid { ref message }
+ if message.contains("bucket key") &&
message.contains("VECTOR")),
+ "altering to a VECTOR bucket key should fail, got {err:?}"
+ );
+ }
+
#[test]
fn test_apply_changes_revalidates_partial_update_options() {
let table_schema = TableSchema::new(
diff --git a/crates/paimon/src/spec/types.rs b/crates/paimon/src/spec/types.rs
index c7a945b..4dbe8e3 100644
--- a/crates/paimon/src/spec/types.rs
+++ b/crates/paimon/src/spec/types.rs
@@ -103,6 +103,8 @@ pub enum DataType {
/// Data type of a sequence of fields. A field consists of a field name,
field type, and an optional
/// description.
Row(RowType),
+ /// Data type of a fixed-size dense vector `VECTOR<element, length>`.
+ Vector(VectorType),
}
impl DataType {
@@ -115,6 +117,7 @@ impl DataType {
DataType::Array(v) => v.element_type.contains_row_type(),
DataType::Map(v) => v.key_type.contains_row_type() ||
v.value_type.contains_row_type(),
DataType::Multiset(v) => v.element_type.contains_row_type(),
+ DataType::Vector(v) => v.element_type.contains_row_type(),
_ => false,
}
}
@@ -147,6 +150,7 @@ impl DataType {
DataType::Map(v) => v.nullable,
DataType::Multiset(v) => v.nullable,
DataType::Row(v) => v.nullable,
+ DataType::Vector(v) => v.nullable,
}
}
@@ -201,6 +205,11 @@ impl DataType {
DataType::Row(v) => {
DataType::Row(RowType::with_nullable(nullable,
v.fields().to_vec()))
}
+ DataType::Vector(v) => DataType::Vector(VectorType::try_new(
+ nullable,
+ v.length(),
+ v.element_type().clone(),
+ )?),
})
}
}
@@ -241,6 +250,202 @@ impl ArrayType {
}
}
+/// VectorType for paimon.
+///
+/// Data type of a fixed-size dense vector `VECTOR<element, length>`. Elements
are densely
+/// stored. The element type must be one of BOOLEAN, TINYINT, SMALLINT, INT,
BIGINT, FLOAT,
+/// DOUBLE, and `length` must be between 1 and `i32::MAX` (both inclusive).
+///
+/// Impl Reference:
<https://github.com/apache/paimon/blob/master/paimon-api/src/main/java/org/apache/paimon/types/VectorType.java>.
+#[serde_as]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
+pub struct VectorType {
+ #[serde(rename = "type")]
+ #[serde_as(as =
"FromInto<serde_utils::NullableType<serde_utils::VECTOR>>")]
+ nullable: bool,
+ #[serde(rename = "element")]
+ element_type: Box<DataType>,
+ length: u32,
+}
+
+impl VectorType {
+ /// Maximum vector length, matching Java `Integer.MAX_VALUE`.
+ pub const MAX_LENGTH: u32 = i32::MAX as u32;
+ /// Minimum vector length.
+ pub const MIN_LENGTH: u32 = 1;
+
+ pub fn new(length: u32, element_type: DataType) -> Result<Self> {
+ Self::try_new(true, length, element_type)
+ }
+
+ pub fn with_nullable(nullable: bool, length: u32, element_type: DataType)
-> Result<Self> {
+ Self::try_new(nullable, length, element_type)
+ }
+
+ pub fn try_new(nullable: bool, length: u32, element_type: DataType) ->
Result<Self> {
+ if !Self::is_valid_element_type(&element_type) {
+ return Err(Error::DataTypeInvalid {
+ message: format!("Invalid element type for vector:
{element_type:?}"),
+ });
+ }
+ if !(Self::MIN_LENGTH..=Self::MAX_LENGTH).contains(&length) {
+ return Err(Error::DataTypeInvalid {
+ message: format!(
+ "Vector length must be between {} and {} (both inclusive),
got {length}.",
+ Self::MIN_LENGTH,
+ Self::MAX_LENGTH
+ ),
+ });
+ }
+ Ok(Self {
+ nullable,
+ element_type: Box::new(element_type),
+ length,
+ })
+ }
+
+ fn is_valid_element_type(dt: &DataType) -> bool {
+ matches!(
+ dt,
+ DataType::Boolean(_)
+ | DataType::TinyInt(_)
+ | DataType::SmallInt(_)
+ | DataType::Int(_)
+ | DataType::BigInt(_)
+ | DataType::Float(_)
+ | DataType::Double(_)
+ )
+ }
+
+ pub fn family(&self) -> DataTypeFamily {
+ DataTypeFamily::CONSTRUCTED | DataTypeFamily::COLLECTION
+ }
+
+ pub fn element_type(&self) -> &DataType {
+ &self.element_type
+ }
+
+ 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)?;
+ if !self.nullable {
+ write!(f, " NOT NULL")?;
+ }
+ Ok(())
+ }
+}
+
+// Hand-written `Deserialize` (rather than `#[derive(Deserialize)]` with
+// `#[serde(try_from = ...)]`): the derive-based `try_from` path does not
compose
+// with the untagged `DataType` enum for a struct carrying a numeric (`length`)
+// field — under `#[serde(untagged)]` serde buffers input into a `Content`
value
+// and replays it through a `ContentDeserializer`, which mis-handles the
+// `serde_with` layering. This visitor parses the Java JSON shape directly and
+// routes through `VectorType::try_new` so validation still runs on
deserialize.
+impl<'de> Deserialize<'de> for VectorType {
+ fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ use serde::de::{self, MapAccess, Visitor};
+ use std::fmt;
+
+ #[derive(Deserialize)]
+ #[serde(field_identifier, rename_all = "lowercase")]
+ enum Field {
+ #[serde(rename = "type")]
+ Type,
+ Element,
+ Length,
+ }
+
+ struct VectorTypeVisitor;
+
+ impl<'de> Visitor<'de> for VectorTypeVisitor {
+ type Value = VectorType;
+
+ fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ f.write_str("a VECTOR data type object")
+ }
+
+ fn visit_map<A>(self, mut map: A) ->
std::result::Result<VectorType, A::Error>
+ where
+ A: MapAccess<'de>,
+ {
+ let mut nullable: Option<bool> = None;
+ let mut element_type: Option<Box<DataType>> = None;
+ let mut length: Option<u32> = None;
+
+ while let Some(key) = map.next_key::<Field>()? {
+ match key {
+ Field::Type => {
+ if nullable.is_some() {
+ return Err(de::Error::duplicate_field("type"));
+ }
+ // Reuse the shared `VECTOR`/`VECTOR NOT NULL`
parsing.
+ let raw:
serde_utils::NullableType<serde_utils::VECTOR> =
+ map.next_value()?;
+ nullable = Some(raw.into());
+ }
+ Field::Element => {
+ if element_type.is_some() {
+ return
Err(de::Error::duplicate_field("element"));
+ }
+ element_type = Some(map.next_value()?);
+ }
+ Field::Length => {
+ if length.is_some() {
+ return
Err(de::Error::duplicate_field("length"));
+ }
+ length = Some(map.next_value()?);
+ }
+ }
+ }
+
+ let nullable = nullable.ok_or_else(||
de::Error::missing_field("type"))?;
+ let element_type =
+ element_type.ok_or_else(||
de::Error::missing_field("element"))?;
+ let length = length.ok_or_else(||
de::Error::missing_field("length"))?;
+
+ VectorType::try_new(nullable, length,
*element_type).map_err(de::Error::custom)
+ }
+ }
+
+ deserializer.deserialize_struct(
+ "VectorType",
+ &["type", "element", "length"],
+ VectorTypeVisitor,
+ )
+ }
+}
+
/// BigIntType for paimon.
///
/// Data type of an 8-byte (2^64) signed integer with values from
-9,223,372,036,854,775,808 to 9,223,372,036,854,775,807.
@@ -1507,6 +1712,11 @@ mod serde_utils {
const NAME: &'static str = "ARRAY";
}
+ pub struct VECTOR;
+ impl DataTypeName for VECTOR {
+ const NAME: &'static str = "VECTOR";
+ }
+
pub struct BIGINT;
impl DataTypeName for BIGINT {
const NAME: &'static str = "BIGINT";
@@ -2276,4 +2486,148 @@ mod tests {
.parse::<i32>()
.expect("VARBINARY length must parse as Java int");
}
+
+ #[test]
+ fn test_vector_type_construction_valid_elements() {
+ for elem in [
+ DataType::Boolean(BooleanType::new()),
+ DataType::TinyInt(TinyIntType::new()),
+ DataType::SmallInt(SmallIntType::new()),
+ DataType::Int(IntType::new()),
+ DataType::BigInt(BigIntType::new()),
+ DataType::Float(FloatType::new()),
+ DataType::Double(DoubleType::new()),
+ ] {
+ let v = VectorType::try_new(true, 8, elem.clone()).unwrap();
+ assert_eq!(v.length(), 8);
+ assert_eq!(v.element_type(), &elem);
+ }
+ }
+
+ #[test]
+ fn test_vector_type_rejects_invalid_element() {
+ let err = VectorType::try_new(true, 4,
DataType::VarChar(VarCharType::new(10).unwrap()));
+ assert!(matches!(err, Err(Error::DataTypeInvalid { .. })));
+ let err = VectorType::try_new(
+ true,
+ 4,
+ DataType::Array(ArrayType::new(DataType::Float(FloatType::new()))),
+ );
+ assert!(matches!(err, Err(Error::DataTypeInvalid { .. })));
+ }
+
+ #[test]
+ fn test_vector_type_rejects_bad_length() {
+ assert!(matches!(
+ VectorType::try_new(true, 0, DataType::Float(FloatType::new())),
+ Err(Error::DataTypeInvalid { .. })
+ ));
+ assert!(VectorType::try_new(true, 1,
DataType::Float(FloatType::new())).is_ok());
+ let too_big = i32::MAX as u32 + 1;
+ assert!(matches!(
+ VectorType::try_new(true, too_big,
DataType::Float(FloatType::new())),
+ Err(Error::DataTypeInvalid { .. })
+ ));
+ }
+
+ #[test]
+ fn test_vector_type_display() {
+ let v = VectorType::try_new(true, 128,
DataType::Float(FloatType::new())).unwrap();
+ assert_eq!(v.to_string(), "VECTOR<FLOAT, 128>");
+ let v = VectorType::try_new(false, 128,
DataType::Float(FloatType::new())).unwrap();
+ assert_eq!(v.to_string(), "VECTOR<FLOAT, 128> NOT NULL");
+
+ // The element's own nullability is rendered too (matches Java
asSQLString and
+ // is consistent with serde, which preserves the element's "FLOAT NOT
NULL").
+ let v =
+ VectorType::try_new(true, 4,
DataType::Float(FloatType::with_nullable(false))).unwrap();
+ assert_eq!(v.to_string(), "VECTOR<FLOAT NOT NULL, 4>");
+ let v = VectorType::try_new(false, 4,
DataType::Float(FloatType::with_nullable(false)))
+ .unwrap();
+ assert_eq!(v.to_string(), "VECTOR<FLOAT NOT NULL, 4> NOT NULL");
+ }
+
+ #[test]
+ fn test_vector_type_serde_roundtrip() {
+ // Atomic element types serialize as bare strings (e.g. "FLOAT"),
matching
+ // the existing ARRAY wire form (see tests/fixtures/array_type.json),
not as
+ // nested objects. Pin the exact JSON so the wire shape cannot drift.
+ let v = VectorType::try_new(true, 3,
DataType::Float(FloatType::new())).unwrap();
+ let json = serde_json::to_string(&v).unwrap();
+ assert_eq!(json, r#"{"type":"VECTOR","element":"FLOAT","length":3}"#);
+ let back: VectorType = serde_json::from_str(&json).unwrap();
+ assert_eq!(back, v);
+
+ let nn = VectorType::try_new(false, 2,
DataType::Double(DoubleType::new())).unwrap();
+ let json = serde_json::to_string(&nn).unwrap();
+ assert_eq!(
+ json,
+ r#"{"type":"VECTOR NOT NULL","element":"DOUBLE","length":2}"#
+ );
+ let back: VectorType = serde_json::from_str(&json).unwrap();
+ assert_eq!(back, nn);
+ }
+
+ #[test]
+ fn test_vector_type_deserialize_rejects_invalid() {
+ // Use the real string-form element so the failure is the vector's own
+ // validation (invalid element type / bad length), not a failure to
parse
+ // the element itself.
+ let bad_elem =
r#"{"type":"VECTOR","element":"VARCHAR(10)","length":4}"#;
+ let err = serde_json::from_str::<VectorType>(bad_elem).unwrap_err();
+ assert!(
+ err.to_string().contains("Invalid element type for vector"),
+ "expected element-type validation error, got: {err}"
+ );
+
+ let bad_len = r#"{"type":"VECTOR","element":"FLOAT","length":0}"#;
+ let err = serde_json::from_str::<VectorType>(bad_len).unwrap_err();
+ assert!(
+ err.to_string().contains("Vector length must be between"),
+ "expected length validation error, got: {err}"
+ );
+ }
+
+ #[test]
+ fn test_datatype_vector_untagged_roundtrip() {
+ let dt = DataType::Vector(
+ VectorType::try_new(true, 4,
DataType::Float(FloatType::new())).unwrap(),
+ );
+ let json = serde_json::to_string(&dt).unwrap();
+ let back: DataType = serde_json::from_str(&json).unwrap();
+ assert_eq!(back, dt);
+ assert!(matches!(back, DataType::Vector(_)));
+
+ // ARRAY must still parse as Array, not Vector. Atomic element types
are
+ // serialized as bare strings (e.g. "FLOAT") in this codebase, not
objects.
+ let arr_json = r#"{"type":"ARRAY","element":"FLOAT"}"#;
+ let arr: DataType = serde_json::from_str(arr_json).unwrap();
+ assert!(matches!(arr, DataType::Array(_)));
+ }
+
+ #[test]
+ fn test_datatype_vector_nullable_and_copy() {
+ let dt = DataType::Vector(
+ VectorType::try_new(true, 4,
DataType::Float(FloatType::new())).unwrap(),
+ );
+ assert!(dt.is_nullable());
+ let nn = dt.copy_with_nullable(false).unwrap();
+ assert!(!nn.is_nullable());
+ if let DataType::Vector(v) = &nn {
+ assert_eq!(v.length(), 4);
+ assert_eq!(v.element_type(), &DataType::Float(FloatType::new()));
+ } else {
+ panic!("expected Vector");
+ }
+ assert!(!dt.contains_row_type());
+ }
+
+ #[test]
+ fn test_datatype_vector_arrow_unsupported() {
+ let dt = DataType::Vector(
+ VectorType::try_new(true, 4,
DataType::Float(FloatType::new())).unwrap(),
+ );
+ let err = crate::arrow::paimon_type_to_arrow(&dt);
+ assert!(matches!(err, Err(crate::Error::Unsupported { .. })));
+ }
}