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

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


The following commit(s) were added to refs/heads/main by this push:
     new 093b31e  feat: Add nullability support in python (#522)
093b31e is described below

commit 093b31e0b5344d66c79391c54dc55bbdf8c3042c
Author: Kaiqi Dong <[email protected]>
AuthorDate: Sun May 3 15:29:35 2026 +0200

    feat: Add nullability support in python (#522)
---
 bindings/python/src/metadata.rs                 |  2 +-
 bindings/python/src/utils.rs                    | 34 ++++++++++++------
 bindings/python/test/test_schema.py             | 47 +++++++++++++++++++++++++
 website/docs/user-guide/python/api-reference.md |  6 ++--
 website/docs/user-guide/python/data-types.md    | 25 +++++++++++++
 5 files changed, 99 insertions(+), 15 deletions(-)

diff --git a/bindings/python/src/metadata.rs b/bindings/python/src/metadata.rs
index 02ef121..7b6129a 100644
--- a/bindings/python/src/metadata.rs
+++ b/bindings/python/src/metadata.rs
@@ -165,7 +165,7 @@ impl Schema {
         let mut builder = fcore::metadata::Schema::builder();
 
         for field in arrow_schema.fields() {
-            let fluss_data_type = 
crate::utils::Utils::arrow_type_to_fluss_type(field.data_type())?;
+            let fluss_data_type = 
crate::utils::Utils::arrow_field_to_fluss_type(field)?;
             builder = builder.column(field.name(), fluss_data_type);
 
             if let Some(comment) = field.metadata().get("comment") {
diff --git a/bindings/python/src/utils.rs b/bindings/python/src/utils.rs
index 76b95f7..5efcf5e 100644
--- a/bindings/python/src/utils.rs
+++ b/bindings/python/src/utils.rs
@@ -36,14 +36,14 @@ impl Utils {
         })
     }
 
-    /// Convert Arrow DataType to Fluss DataType
-    pub fn arrow_type_to_fluss_type(
-        arrow_type: &arrow::datatypes::DataType,
+    /// Convert an Arrow Field to a Fluss DataType, preserving nullability.
+    pub fn arrow_field_to_fluss_type(
+        field: &arrow::datatypes::Field,
     ) -> PyResult<fcore::metadata::DataType> {
         use arrow::datatypes::DataType as ArrowDataType;
         use fcore::metadata::DataTypes;
 
-        let fluss_type = match arrow_type {
+        let fluss_type = match field.data_type() {
             ArrowDataType::Boolean => DataTypes::boolean(),
             ArrowDataType::Int8 => DataTypes::tinyint(),
             ArrowDataType::Int16 => DataTypes::smallint(),
@@ -95,23 +95,29 @@ impl Utils {
             ArrowDataType::Decimal128(precision, scale) => {
                 DataTypes::decimal(*precision as u32, *scale as u32)
             }
-            ArrowDataType::List(field) => {
-                let element_type = 
Utils::arrow_type_to_fluss_type(field.data_type())?;
+            ArrowDataType::List(element_field) => {
+                let element_type = 
Utils::arrow_field_to_fluss_type(element_field)?;
                 DataTypes::array(element_type)
             }
-            _ => {
+            other => {
                 return Err(FlussError::new_err(format!(
-                    "Unsupported Arrow data type: {arrow_type:?}"
+                    "Unsupported Arrow data type: {other:?}"
                 )));
             }
         };
 
-        Ok(fluss_type)
+        if field.is_nullable() {
+            Ok(fluss_type)
+        } else {
+            Ok(fluss_type.as_non_nullable())
+        }
     }
 
-    /// Convert Fluss DataType to string representation
+    /// Convert Fluss DataType to string representation, appending " NOT NULL"
+    /// for non-nullable types (matches Java's `withNullability` and Rust 
core's
+    /// `Display` impl).
     pub fn datatype_to_string(data_type: &fcore::metadata::DataType) -> String 
{
-        match data_type {
+        let type_str = match data_type {
             fcore::metadata::DataType::Boolean(_) => "boolean".to_string(),
             fcore::metadata::DataType::TinyInt(_) => "tinyint".to_string(),
             fcore::metadata::DataType::SmallInt(_) => "smallint".to_string(),
@@ -171,6 +177,12 @@ impl Utils {
                     .collect();
                 format!("row<{}>", fields.join(", "))
             }
+        };
+
+        if data_type.is_nullable() {
+            type_str
+        } else {
+            format!("{type_str} NOT NULL")
         }
     }
 
diff --git a/bindings/python/test/test_schema.py 
b/bindings/python/test/test_schema.py
index ab2d1ab..dfd9cf5 100644
--- a/bindings/python/test/test_schema.py
+++ b/bindings/python/test/test_schema.py
@@ -48,3 +48,50 @@ def test_schema_with_array():
     assert schema.get_column_types() == ["int", "array<string>"]
 
 
+def test_nullable_fields():
+    fields = pa.schema(
+        [
+            pa.field("id", pa.int32(), nullable=False),
+            pa.field("name", pa.string()),
+        ]
+    )
+    schema = fluss.Schema(fields)
+    assert schema.get_column_types() == ["int NOT NULL", "string"]
+    assert schema.get_columns() == [("id", "int NOT NULL"), ("name", "string")]
+
+
+def test_pk_forces_non_nullable():
+    fields = pa.schema(
+        [
+            pa.field("id", pa.int32()),
+            pa.field("name", pa.string()),
+        ]
+    )
+    schema = fluss.Schema(fields, primary_keys=["id"])
+    types = schema.get_column_types()
+    assert types[0] == "int NOT NULL"
+    assert types[1] == "string"
+
+
+def test_nested_list_nullability():
+    fields = pa.schema(
+        [
+            pa.field(
+                "tags",
+                pa.list_(pa.field("item", pa.string(), nullable=False)),
+            ),
+            pa.field("ids", pa.list_(pa.int32()), nullable=False),
+            pa.field(
+                "strict_ids",
+                pa.list_(pa.field("item", pa.int32(), nullable=False)),
+                nullable=False,
+            ),
+        ]
+    )
+    schema = fluss.Schema(fields)
+    types = schema.get_column_types()
+    assert types[0] == "array<string NOT NULL>"
+    assert types[1] == "array<int> NOT NULL"
+    assert types[2] == "array<int NOT NULL> NOT NULL"
+
+
diff --git a/website/docs/user-guide/python/api-reference.md 
b/website/docs/user-guide/python/api-reference.md
index 317aee7..32f23a5 100644
--- a/website/docs/user-guide/python/api-reference.md
+++ b/website/docs/user-guide/python/api-reference.md
@@ -242,10 +242,10 @@ for record in scan_records:
 
 | Method                                         |  Description               |
 |------------------------------------------------|----------------------------|
-| `Schema(schema: pa.Schema, primary_keys=None)` | Create from PyArrow schema |
+| `Schema(schema: pa.Schema, primary_keys=None)` | Create from PyArrow schema. 
Field nullability (`pa.field(..., nullable=False)`) is preserved. |
 | `.get_column_names() -> list[str]`             | Get column names           |
-| `.get_column_types() -> list[str]`             | Get column type names      |
-| `.get_columns() -> list[tuple[str, str]]`      | Get `(name, type)` pairs   |
+| `.get_column_types() -> list[str]`             | Get column type names. 
Non-nullable types include a `" NOT NULL"` suffix (e.g., `"int NOT NULL"`). |
+| `.get_columns() -> list[tuple[str, str]]`      | Get `(name, type)` pairs. 
Type strings follow the same nullability formatting as `.get_column_types()`. |
 | `.get_primary_keys() -> list[str]`             | Get primary key columns    |
 
 ## `TableDescriptor`
diff --git a/website/docs/user-guide/python/data-types.md 
b/website/docs/user-guide/python/data-types.md
index df8165f..9967703 100644
--- a/website/docs/user-guide/python/data-types.md
+++ b/website/docs/user-guide/python/data-types.md
@@ -21,6 +21,31 @@ The Python client uses PyArrow types for schema definitions:
 
 All Python native types (`date`, `time`, `datetime`, `Decimal`) work when 
appending rows via dicts.
 
+## Nullability
+
+PyArrow field nullability is preserved when constructing Fluss schemas. By 
default, fields are nullable. Use `nullable=False` on `pa.field()` to create a 
`NOT NULL` column:
+
+```python
+schema = pa.schema([
+    pa.field("id", pa.int32(), nullable=False),
+    pa.field("name", pa.string()),          # nullable by default
+])
+fluss_schema = fluss.Schema(schema)
+fluss_schema.get_column_types()  # ["int NOT NULL", "string"]
+```
+
+Primary key columns are automatically forced `NOT NULL` regardless of the 
PyArrow field setting.
+
+For nested types, element nullability is also preserved:
+
+```python
+schema = pa.schema([
+    pa.field("tags", pa.list_(pa.field("item", pa.string(), nullable=False))),
+])
+fluss_schema = fluss.Schema(schema)
+fluss_schema.get_column_types()  # ["array<string NOT NULL>"]
+```
+
 ## Writing Data
 
 Rows can be dicts, lists, or tuples:

Reply via email to