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

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


The following commit(s) were added to refs/heads/main by this push:
     new 9dd1a56c4 [client] Add support of column aliasing for PojoConverter 
(#3444)
9dd1a56c4 is described below

commit 9dd1a56c439e1bfc0527db7d452244478c82d5fc
Author: Vladyslav Banar <[email protected]>
AuthorDate: Mon Jun 22 16:15:33 2026 +0200

    [client] Add support of column aliasing for PojoConverter (#3444)
    
    * [client] Add support of column aliasing for PojoConverter
    
    * [client] test
    
    * [client] updated doc
    
    * [client] fix checks
    
    * [client] address comments
    
    ---------
    
    Co-authored-by: valdbanar5 <[email protected]>
---
 .../apache/fluss/client/converter/ColumnName.java  | 39 ++++++++++++++
 .../apache/fluss/client/converter/PojoType.java    | 52 ++++++++++++++----
 .../client/converter/ConvertersTestFixtures.java   | 13 ++++-
 .../fluss/client/converter/PojoTypeTest.java       | 63 ++++++++++++++++++++++
 .../client/converter/RowToPojoConverterTest.java   |  2 +-
 website/docs/apis/java-client.md                   | 15 +++---
 6 files changed, 165 insertions(+), 19 deletions(-)

diff --git 
a/fluss-client/src/main/java/org/apache/fluss/client/converter/ColumnName.java 
b/fluss-client/src/main/java/org/apache/fluss/client/converter/ColumnName.java
new file mode 100644
index 000000000..f3c040717
--- /dev/null
+++ 
b/fluss-client/src/main/java/org/apache/fluss/client/converter/ColumnName.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.fluss.client.converter;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation to specify a custom table column name for a POJO field. When 
present, the converter
+ * will map between the POJO field and the table column using the specified 
name.
+ */
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ColumnName {
+    /**
+     * The name of the table column that this field maps to.
+     *
+     * @return the column name in the Fluss table
+     */
+    String value();
+}
diff --git 
a/fluss-client/src/main/java/org/apache/fluss/client/converter/PojoType.java 
b/fluss-client/src/main/java/org/apache/fluss/client/converter/PojoType.java
index 9a60efd98..bf57a3299 100644
--- a/fluss-client/src/main/java/org/apache/fluss/client/converter/PojoType.java
+++ b/fluss-client/src/main/java/org/apache/fluss/client/converter/PojoType.java
@@ -30,6 +30,8 @@ import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
 
+import static org.apache.fluss.utils.Preconditions.checkArgument;
+
 /**
  * Internal representation of a POJO type, used to validate POJO requirements 
and to provide unified
  * accessors for reading/writing properties.
@@ -73,39 +75,59 @@ final class PojoType<T> {
 
         Map<String, Property> props = new LinkedHashMap<>();
         for (Map.Entry<String, Field> e : allFields.entrySet()) {
-            String name = e.getKey();
+            String fieldName = e.getKey();
             Field field = e.getValue();
             // Enforce nullable fields: primitives are not allowed in POJO 
definitions.
             if (field.getType().isPrimitive()) {
                 throw new IllegalArgumentException(
                         String.format(
                                 "POJO class %s has primitive field '%s' of 
type %s. Primitive types are not allowed; all fields must be nullable (use 
wrapper types).",
-                                pojoClass.getName(), name, 
field.getType().getName()));
+                                pojoClass.getName(), fieldName, 
field.getType().getName()));
             }
+            // Check for @ColumnName annotation to determine the mapped column 
name
+            ColumnName columnNameAnnotation = 
field.getAnnotation(ColumnName.class);
+            String mappedColumnName =
+                    columnNameAnnotation != null ? 
columnNameAnnotation.value() : fieldName;
+            checkArgument(
+                    !mappedColumnName.isEmpty(),
+                    "Column name cannot be empty for field '%s' in POJO class 
%s",
+                    fieldName,
+                    pojoClass.getName());
+
             // use boxed type as effective type
             Class<?> effectiveType = boxIfPrimitive(field.getType());
             boolean publicField = Modifier.isPublic(field.getModifiers());
-            Method getter = getters.get(name);
-            Method setter = setters.get(name);
+            Method getter = getters.get(fieldName);
+            Method setter = setters.get(fieldName);
             if (!publicField) {
                 // When not a public field, require both getter and setter
                 if (getter == null || setter == null) {
-                    final String capitalizedName = capitalize(name);
+                    final String capitalizedName = capitalize(fieldName);
                     throw new IllegalArgumentException(
                             String.format(
                                     "POJO class %s field '%s' must be public 
or have both getter and setter (get%s/set%s).",
-                                    pojoClass.getName(), name, 
capitalizedName, capitalizedName));
+                                    pojoClass.getName(),
+                                    fieldName,
+                                    capitalizedName,
+                                    capitalizedName));
                 }
             }
+            if (props.get(mappedColumnName) != null) {
+                throw new IllegalArgumentException(
+                        String.format(
+                                "Duplicated property name '%s' in class %s",
+                                mappedColumnName, pojoClass.getName()));
+            }
             props.put(
-                    name,
+                    mappedColumnName,
                     new Property(
-                            name,
+                            fieldName,
                             effectiveType,
                             field.getGenericType(),
                             publicField ? field : null,
                             getter,
-                            setter));
+                            setter,
+                            mappedColumnName));
         }
 
         return new PojoType<>(pojoClass, ctor, props);
@@ -269,11 +291,19 @@ final class PojoType<T> {
     }
 
     static final class Property {
+        /** The name of the field in the POJO class (e.g. "userId"). */
         final String name;
+
         final Class<?> type;
         /** The generic type of the field (e.g. {@code Map<String, 
AddressPojo>}). */
         final Type genericType;
 
+        /**
+         * The name of the column in the Fluss table. This may differ from 
'name' if a @ColumnName
+         * annotation is present. Used for looking up the property by table 
column name.
+         */
+        final String mappedName;
+
         @Nullable final Field publicField;
         @Nullable final Method getter;
         @Nullable final Method setter;
@@ -284,10 +314,12 @@ final class PojoType<T> {
                 Type genericType,
                 @Nullable Field publicField,
                 @Nullable Method getter,
-                @Nullable Method setter) {
+                @Nullable Method setter,
+                String mappedName) {
             this.name = Objects.requireNonNull(name, "name");
             this.type = Objects.requireNonNull(type, "type");
             this.genericType = Objects.requireNonNull(genericType, 
"genericType");
+            this.mappedName = Objects.requireNonNull(mappedName, "mappedName");
             this.publicField = publicField;
             this.getter = getter;
             this.setter = setter;
diff --git 
a/fluss-client/src/test/java/org/apache/fluss/client/converter/ConvertersTestFixtures.java
 
b/fluss-client/src/test/java/org/apache/fluss/client/converter/ConvertersTestFixtures.java
index 91d2d11cd..0a90c522d 100644
--- 
a/fluss-client/src/test/java/org/apache/fluss/client/converter/ConvertersTestFixtures.java
+++ 
b/fluss-client/src/test/java/org/apache/fluss/client/converter/ConvertersTestFixtures.java
@@ -57,6 +57,7 @@ public final class ConvertersTestFixtures {
                 .field("offsetDateTimeField", DataTypes.TIMESTAMP_LTZ())
                 .field("arrayField", DataTypes.ARRAY(DataTypes.INT()))
                 .field("mapField", DataTypes.MAP(DataTypes.STRING(), 
DataTypes.INT()))
+                .field("string_with_column_name", DataTypes.STRING())
                 .build();
     }
 
@@ -82,6 +83,9 @@ public final class ConvertersTestFixtures {
         public Integer[] arrayField;
         public Map<String, Integer> mapField;
 
+        @ColumnName("string_with_column_name")
+        public String stringWithColumnName;
+
         public TestPojo() {}
 
         public TestPojo(
@@ -101,7 +105,8 @@ public final class ConvertersTestFixtures {
                 Instant timestampLtzField,
                 OffsetDateTime offsetDateTimeField,
                 Integer[] arrayField,
-                Map<String, Integer> mapField) {
+                Map<String, Integer> mapField,
+                String stringWithColumnName) {
             this.booleanField = booleanField;
             this.byteField = byteField;
             this.shortField = shortField;
@@ -119,6 +124,7 @@ public final class ConvertersTestFixtures {
             this.offsetDateTimeField = offsetDateTimeField;
             this.arrayField = arrayField;
             this.mapField = mapField;
+            this.stringWithColumnName = stringWithColumnName;
         }
 
         public static TestPojo sample() {
@@ -144,7 +150,8 @@ public final class ConvertersTestFixtures {
                             put("test_1", 1);
                             put("test_2", 2);
                         }
-                    });
+                    },
+                    "string value");
         }
 
         @Override
@@ -171,6 +178,7 @@ public final class ConvertersTestFixtures {
                     && Objects.equals(timestampField, testPojo.timestampField)
                     && Objects.equals(timestampLtzField, 
testPojo.timestampLtzField)
                     && Objects.equals(offsetDateTimeField, 
testPojo.offsetDateTimeField)
+                    && Objects.equals(stringWithColumnName, 
testPojo.stringWithColumnName)
                     && Arrays.equals(arrayField, testPojo.arrayField)
                     && Objects.equals(mapField, testPojo.mapField);
         }
@@ -192,6 +200,7 @@ public final class ConvertersTestFixtures {
                             timeField,
                             timestampField,
                             timestampLtzField,
+                            stringWithColumnName,
                             offsetDateTimeField,
                             mapField);
             result = 31 * result + Arrays.hashCode(bytesField);
diff --git 
a/fluss-client/src/test/java/org/apache/fluss/client/converter/PojoTypeTest.java
 
b/fluss-client/src/test/java/org/apache/fluss/client/converter/PojoTypeTest.java
index e2d44f581..2d261dc50 100644
--- 
a/fluss-client/src/test/java/org/apache/fluss/client/converter/PojoTypeTest.java
+++ 
b/fluss-client/src/test/java/org/apache/fluss/client/converter/PojoTypeTest.java
@@ -19,6 +19,7 @@ package org.apache.fluss.client.converter;
 
 import org.junit.jupiter.api.Test;
 
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 /** Basic tests for {@link PojoType}. */
@@ -61,6 +62,34 @@ class PojoTypeTest {
         PojoType.of(PublicWithPublicNonPrimitive.class);
     }
 
+    @Test
+    void testColumnNameAnnotation() {
+        PojoType<PojoWithColumnName> pojoType = 
PojoType.of(PojoWithColumnName.class);
+
+        assertThat(pojoType.getProperty("user_id"))
+                .isNotNull()
+                .hasFieldOrPropertyWithValue("name", "userId")
+                .hasFieldOrPropertyWithValue("mappedName", "user_id");
+
+        assertThat(pojoType.getProperty("first_name"))
+                .isNotNull()
+                .hasFieldOrPropertyWithValue("name", "firstName")
+                .hasFieldOrPropertyWithValue("mappedName", "first_name");
+
+        // Fields without @ColumnName should map to themselves
+        assertThat(pojoType.getProperty("email"))
+                .isNotNull()
+                .hasFieldOrPropertyWithValue("name", "email")
+                .hasFieldOrPropertyWithValue("mappedName", "email");
+    }
+
+    @Test
+    void testColumnNameAnnotationWithDuplicate() {
+        assertThatThrownBy(() -> 
PojoType.of(PojoWithDuplicateColumnName.class))
+                .isExactlyInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("Duplicated property name");
+    }
+
     public class ClassWithNoPublicConstructor {
         int f;
         int j;
@@ -160,4 +189,38 @@ class PojoTypeTest {
             this.b = b;
         }
     }
+
+    public static class PojoWithColumnName {
+        @ColumnName("user_id")
+        public Long userId;
+
+        @ColumnName("first_name")
+        private String firstName;
+
+        public String email;
+
+        public String getFirstName() {
+            return firstName;
+        }
+
+        public void setFirstName(String firstName) {
+            this.firstName = firstName;
+        }
+    }
+
+    public static class PojoWithDuplicateColumnName {
+        @ColumnName("first_name")
+        public String userId;
+
+        @ColumnName("first_name")
+        private String firstName;
+
+        public String getFirstName() {
+            return firstName;
+        }
+
+        public void setFirstName(String firstName) {
+            this.firstName = firstName;
+        }
+    }
 }
diff --git 
a/fluss-client/src/test/java/org/apache/fluss/client/converter/RowToPojoConverterTest.java
 
b/fluss-client/src/test/java/org/apache/fluss/client/converter/RowToPojoConverterTest.java
index 92fe60267..0d849a61e 100644
--- 
a/fluss-client/src/test/java/org/apache/fluss/client/converter/RowToPojoConverterTest.java
+++ 
b/fluss-client/src/test/java/org/apache/fluss/client/converter/RowToPojoConverterTest.java
@@ -44,7 +44,7 @@ public class RowToPojoConverterTest {
 
         ConvertersTestFixtures.TestPojo pojo = 
ConvertersTestFixtures.TestPojo.sample();
         GenericRow row = writer.toRow(pojo);
-        assertThat(row.getFieldCount()).isEqualTo(17);
+        assertThat(row.getFieldCount()).isEqualTo(18);
 
         ConvertersTestFixtures.TestPojo back = scanner.fromRow(row);
         assertThat(back).isEqualTo(pojo);
diff --git a/website/docs/apis/java-client.md b/website/docs/apis/java-client.md
index eeb169f81..483296b0c 100644
--- a/website/docs/apis/java-client.md
+++ b/website/docs/apis/java-client.md
@@ -336,20 +336,23 @@ The Typed API provides a more user-friendly experience 
but comes with a performa
 
 ### Defining POJOs
 
-To use the Typed API, define a Java class where the field names and types 
match your Fluss table schema.
+To use the Typed API, define a Java class where the field names and types 
match your Fluss table schema. You can use
+@ColumnName to properly map the name.
 
 ```java
 public class User {
     public Integer id;
-    public String name;
-    public Integer age;
+    @ColumnName("first_name")
+    public String firstName;
+    @ColumnName("last_name")
+    public String lastName;
 
     public User() {}
 
-    public User(Integer id, String name, Integer age) {
+    public User(Integer id, String firstName, String lastName) {
         this.id = id;
-        this.name = name;
-        this.age = age;
+        this.firstName = firstName;
+        this.lastName = lastName;
     }
     
     // Getters, setters, equals, hashCode, toString...

Reply via email to