You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@ignite.apache.org by GitBox <gi...@apache.org> on 2021/11/17 16:00:41 UTC

[GitHub] [ignite-3] AMashenkov opened a new pull request #455: IGNITE-15784 Mapper support arbitrary serializable user pojo.

AMashenkov opened a new pull request #455:
URL: https://github.com/apache/ignite-3/pull/455


   https://issues.apache.org/jira/browse/IGNITE-15784


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [ignite-3] ptupitsyn commented on a change in pull request #455: IGNITE-15784 Mapper support arbitrary serializable user pojo.

Posted by GitBox <gi...@apache.org>.
ptupitsyn commented on a change in pull request #455:
URL: https://github.com/apache/ignite-3/pull/455#discussion_r767708455



##########
File path: modules/api/src/main/java/org/apache/ignite/table/KeyValueView.java
##########
@@ -37,19 +38,76 @@
     /**
      * Gets a value associated with the given key.
      *
+     * <p>Note: If the value mapper implies a value can be {@code null}, then a suitable method
+     * {@link #getNullable(Object)} must be used instead.
+     *
      * @param key A key which associated the value is to be returned. The key cannot be {@code null}.
      * @return Value or {@code null}, if it does not exist.
+     * @throws IllegalStateException If value for the key exists, and it is {@code null}.
+     * @see #getNullable(Object)
      */
     V get(@NotNull K key);
 
     /**
      * Asynchronously gets a value associated with the given key.
      *
+     * <p>Note: If the value mapper implies a value can be {@code null}, then a suitable method
+     * {@link #getNullableAsync(Object)} must be used instead.
+     *
      * @param key A key which associated the value is to be returned. The key cannot be {@code null}.
      * @return Future representing pending completion of the operation.
+     * @see #getNullableAsync(Object)
+     * @see #get(Object)
      */
     @NotNull CompletableFuture<V> getAsync(@NotNull K key);
 
+    /**
+     * Gets a nullable value associated with the given key.
+     *
+     * @param key A key which associated the value is to be returned. The key cannot be {@code null}.
+     * @return Wrapped nullable value or {@code null}, if it does not exist.
+     */
+    default NullableValue<V> getNullable(K key) {
+        throw new UnsupportedOperationException("Not implemented yet.");

Review comment:
       Can you please add a comment with the ticket number?

##########
File path: modules/api/src/main/java/org/apache/ignite/table/mapper/Mapper.java
##########
@@ -17,61 +17,178 @@
 
 package org.apache.ignite.table.mapper;
 
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
+import java.lang.reflect.Modifier;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.BitSet;
+import java.util.UUID;
 
 /**
  * Mapper interface defines methods that are required for a marshaller to map class field names to table columns.
  *
- * @param <T> Mapped type.
+ * <p>NB: Anonymous, local, inner classes, and special types are NOT supported, and mapper will not be created for them: e.g. interfaces,
+ * annotation, and so on.
+ *
+ * @param <T> Type of which objects the mapper handles.
+ * @apiNote Implementation shouldn't use this interface directly, please, use {@link PojoMapper} or {@link OneColumnMapper}
+ *         instead.
+ * @see PojoMapper
+ * @see OneColumnMapper
  */
 public interface Mapper<T> {
     /**
-     * Creates a mapper for a class.
+     * Shortcut method creates a mapper for class. Natively supported types will be mapped to a single schema column, otherwise individual
+     * object fields will be mapped to columns with the same name.
+     *
+     * <p>Note: Natively supported types can be mapped to a single key/value column, otherwise table operation will ends up with exception.
+     * Use {@link #of(Class, String)} instead, to map to a concrete column name.
      *
-     * @param cls Key class.
-     * @param <K> Key type.
-     * @return Mapper.
+     * @param cls Target type.
+     * @return Mapper for key objects representing a single key column.
+     * @throws IllegalArgumentException If {@code cls} is of unsupported kind.
      */
-    static <K> Mapper<K> of(Class<K> cls) {
-        return identity(cls);
+    static <O> Mapper<O> of(Class<O> cls) {
+        if (nativelySupported(cls)) {
+            // TODO: Cache mappers (IGNITE-16094).
+            return new OneColumnMapperImpl<>(cls, null, null);
+        } else {
+            return builder(cls).automap().build();
+        }
     }
 
     /**
-     * Creates a mapper builder for a class.
+     * Creates a mapper for the case when an object represents an only one-column.
+     *
+     * <p>The mapper can be used as key, value, or record mapper. However, single column record looks as degraded case.

Review comment:
       ```suggestion
        * <p>The mapper can be used as a key, value, or record mapper. However, a single-column record looks like a degraded case.
   ```

##########
File path: modules/api/src/main/java/org/apache/ignite/table/mapper/MapperBuilder.java
##########
@@ -37,93 +50,205 @@
     /** Column-to-field name mapping. */
     private Map<String, String> columnToFields;
 
+    /** Column converters. */
+    private Map<String, TypeConverter<?, ?>> columnConverters = new HashMap<>();
+
+    /** Column name for one-column mapping. */
+    private final String mappedToColumn;
+
+    /** Flag indicates, if all unmapped object fields should be mapped to the columns with same names automatically. */
+    private boolean automapFlag = false;
+
+    /** {@code True} if the {@link #build()} method was called, {@code false} otherwise. */
+    private boolean isStale;
+
     /**
-     * Creates a mapper builder for a type.
+     * Creates a mapper builder for a POJO type.
      *
      * @param targetType Target type.
      */
     MapperBuilder(@NotNull Class<T> targetType) {
-        this.targetType = targetType;
+        this.targetType = Mapper.ensureValidPojo(targetType);
 
+        mappedToColumn = null;
         columnToFields = new HashMap<>(targetType.getDeclaredFields().length);
     }
 
     /**
-     * Add mapping for a field to a column.
+     * Creates a mapper builder for a natively supported type.
+     *
+     * @param targetType   Target type.
+     * @param mappedColumn Column name to map to.
+     */
+    MapperBuilder(@NotNull Class<T> targetType, String mappedColumn) {
+        this.targetType = Mapper.ensureNativelySupported(targetType);
+
+        mappedToColumn = mappedColumn;
+        columnToFields = null;
+    }
+
+    /**
+     * Ensure this instance is valid.
      *
-     * @param fieldName  Field name.
-     * @param columnName Column name.
-     * @return {@code this} for chaining.
-     * @throws IllegalArgumentException if a column was already mapped to some field.
      * @throws IllegalStateException if tries to reuse the builder after a mapping has been built.
      */
-    public MapperBuilder<T> map(@NotNull String fieldName, @NotNull String columnName) {
-        if (columnToFields == null) {
+    private void ensureNotStale() {
+        if (isStale) {
             throw new IllegalStateException("Mapper builder can't be reused.");
         }
+    }
+
+    /**
+     * Ensure field name is valid and field with this name exists.
+     *
+     * @param fieldName Field name.
+     * @return Field name for chaining.
+     * @throws IllegalArgumentException If field is {@code null} or class has no declared field with given name.
+     */
+    private String requireValidField(String fieldName) {
+        try {
+            if (fieldName == null || targetType.getDeclaredField(fieldName) == null) {
+                throw new IllegalArgumentException("Mapping for a column already exists: " + fieldName);
+            }
+        } catch (NoSuchFieldException e) {
+            throw new IllegalArgumentException(
+                    String.format("Field not found for class: field=%s, class=%s", fieldName, targetType.getName()));
+        }
+
+        return fieldName;
+    }
+
+    /**
+     * Maps a field to a column.
+     *
+     * @param fieldName        Field name.
+     * @param columnName       Column name.
+     * @param fieldColumnPairs Vararg that accepts (fieldName, columnName) pairs.
+     * @return {@code this} for chaining.
+     * @throws IllegalArgumentException If a field name has not paired column name in {@code fieldColumnPairs}, or a column was already
+     *                                  mapped to another field.
+     * @throws IllegalStateException    if tries to reuse the builder after a mapping has been built.
+     */
+    public MapperBuilder<T> map(@NotNull String fieldName, @NotNull String columnName, String... fieldColumnPairs) {
+        ensureNotStale();
 
-        if (columnToFields.put(Objects.requireNonNull(columnName), Objects.requireNonNull(fieldName)) != null) {
+        if (columnToFields == null) {
+            throw new IllegalArgumentException("Natively supported types doesn't support field mapping.");
+        } else if (fieldColumnPairs.length % 2 != 0) {
+            throw new IllegalArgumentException("Missed a column name, which the field is mapped to.");
+        } else if (columnToFields.put(Objects.requireNonNull(columnName), requireValidField(fieldName)) != null) {
             throw new IllegalArgumentException("Mapping for a column already exists: " + columnName);
         }
 
+        for (int i = 0; i < fieldColumnPairs.length; i += 2) {
+            if (columnToFields.put(Objects.requireNonNull(fieldColumnPairs[i + 1]), requireValidField(fieldColumnPairs[i])) != null) {
+                throw new IllegalArgumentException("Mapping for a column already exists: " + columnName);
+            }
+        }
+
         return this;
     }
 
     /**
-     * Map a field to a type of given class.
+     * Maps a field to a column with using type converter. The value will be converted before write to and after read from column using
+     * provided converter.
      *
-     * @param fieldName   Field name.
-     * @param targetClass Target class.
-     * @return {@code this} for chaining.
+     * @param <ObjectT>  MUST match the object field type, if the individual field mapped to given column.
+     * @param <ColumnT>  MUST be a type, which compatible with the column type.
+     * @param fieldName  Field name.
+     * @param columnName Column name.
+     * @param converter  Converter for objects of {@link ColumnT} and {@link ObjectT}.
      */
-    public MapperBuilder<T> map(@NotNull String fieldName, Class<?> targetClass) {
-        throw new UnsupportedOperationException("Not implemented yet.");
+    public <ObjectT, ColumnT> MapperBuilder<T> map(
+            @NotNull String fieldName,
+            @NotNull String columnName,
+            @NotNull TypeConverter<ObjectT, ColumnT> converter
+    ) {
+        map(fieldName, columnName);
+        convert(converter, columnName);
+
+        return this;
     }
 
     /**
-     * Adds a functional mapping for a field, the result depends on function call for every particular row.
+     * Adds a manual functional mapping for an object and row represented by tuple.
      *
-     * @param fieldName       Field name.
-     * @param mappingFunction Mapper function.
+     * @param objectToRow Object to tuple function.
+     * @param rowToObject Tuple to object function.
      * @return {@code this} for chaining.
      */
-    public MapperBuilder<T> map(@NotNull String fieldName, Function<Tuple, Object> mappingFunction) {
+    public MapperBuilder<T> map(Function<T, Tuple> objectToRow, Function<Tuple, T> rowToObject) {
+        ensureNotStale();
+
         throw new UnsupportedOperationException("Not implemented yet.");
     }
 
     /**
-     * Sets a target class to deserialize to.
+     * Sets a converter for a column, which value must be converted before write/after read.
+     *
+     * @param converter  Converter for objects of {@link ColumnT} and {@link ObjectT}.
+     * @param columnName Column name.
+     * @param <ObjectT>  MUST match the object field type, if a field mapped to given column, or the object type {@link T}
+     * @param <ColumnT>  MUST be a type, which compatible with the column type.
+     */
+    public <ObjectT, ColumnT> MapperBuilder<T> convert(
+            @NotNull TypeConverter<ObjectT, ColumnT> converter,
+            @NotNull String columnName
+    ) {
+        ensureNotStale();
+
+        if (columnConverters.put(columnName, converter) != null) {
+            throw new IllegalArgumentException("Column converter already exists: " + columnName);
+        }
+
+        return this;
+    }
+
+    /**
+     * Make mapper treat missed columns as they mapped to the field of the same name. If class {@link T} has no field for the missed column,
+     * then left the column unmapped.
      *
-     * @param targetClass Target class.
      * @return {@code this} for chaining.
      */
-    public MapperBuilder<T> deserializeTo(@NotNull Class<?> targetClass) {
-        throw new UnsupportedOperationException("Not implemented yet.");
+    public MapperBuilder<T> automap() {
+        ensureNotStale();
+
+        automapFlag = true;
+
+        return this;
     }
 
     /**
      * Builds mapper.
      *
      * @return Mapper.
-     * @throws IllegalStateException if nothing were mapped or more than one column were mapped to the same field.
+     * @throws IllegalStateException if nothing were mapped, or more than one column were mapped to the same field.

Review comment:
       ```suggestion
        * @throws IllegalStateException if nothing was mapped, or more than one column was mapped to the same field.
   ```

##########
File path: modules/api/src/main/java/org/apache/ignite/table/mapper/Mapper.java
##########
@@ -17,61 +17,178 @@
 
 package org.apache.ignite.table.mapper;
 
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
+import java.lang.reflect.Modifier;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.BitSet;
+import java.util.UUID;
 
 /**
  * Mapper interface defines methods that are required for a marshaller to map class field names to table columns.
  *
- * @param <T> Mapped type.
+ * <p>NB: Anonymous, local, inner classes, and special types are NOT supported, and mapper will not be created for them: e.g. interfaces,

Review comment:
       What will happen in this case? An exception?

##########
File path: modules/api/src/main/java/org/apache/ignite/lang/NullableValue.java
##########
@@ -0,0 +1,35 @@
+/*
+ * 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.ignite.lang;
+
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Nullable value interface, which instances wraps real value, is used to distinct cases: 'value is absent', and 'value is {@code null}'.

Review comment:
       ```suggestion
    * Represents a value that can be {@code null}. Used to distinguish 'value is absent' and `value is null` cases.
   ```

##########
File path: modules/api/src/main/java/org/apache/ignite/table/mapper/Mapper.java
##########
@@ -17,61 +17,178 @@
 
 package org.apache.ignite.table.mapper;
 
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
+import java.lang.reflect.Modifier;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.BitSet;
+import java.util.UUID;
 
 /**
  * Mapper interface defines methods that are required for a marshaller to map class field names to table columns.
  *
- * @param <T> Mapped type.
+ * <p>NB: Anonymous, local, inner classes, and special types are NOT supported, and mapper will not be created for them: e.g. interfaces,
+ * annotation, and so on.
+ *
+ * @param <T> Type of which objects the mapper handles.
+ * @apiNote Implementation shouldn't use this interface directly, please, use {@link PojoMapper} or {@link OneColumnMapper}
+ *         instead.
+ * @see PojoMapper
+ * @see OneColumnMapper
  */
 public interface Mapper<T> {
     /**
-     * Creates a mapper for a class.
+     * Shortcut method creates a mapper for class. Natively supported types will be mapped to a single schema column, otherwise individual

Review comment:
       ```suggestion
        * Creates a mapper for the specified class. Natively supported types will be mapped to a single schema column, otherwise individual
   ```

##########
File path: modules/api/src/main/java/org/apache/ignite/table/mapper/Mapper.java
##########
@@ -17,61 +17,178 @@
 
 package org.apache.ignite.table.mapper;
 
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
+import java.lang.reflect.Modifier;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.BitSet;
+import java.util.UUID;
 
 /**
  * Mapper interface defines methods that are required for a marshaller to map class field names to table columns.
  *
- * @param <T> Mapped type.
+ * <p>NB: Anonymous, local, inner classes, and special types are NOT supported, and mapper will not be created for them: e.g. interfaces,
+ * annotation, and so on.
+ *
+ * @param <T> Type of which objects the mapper handles.
+ * @apiNote Implementation shouldn't use this interface directly, please, use {@link PojoMapper} or {@link OneColumnMapper}
+ *         instead.
+ * @see PojoMapper
+ * @see OneColumnMapper
  */
 public interface Mapper<T> {
     /**
-     * Creates a mapper for a class.
+     * Shortcut method creates a mapper for class. Natively supported types will be mapped to a single schema column, otherwise individual
+     * object fields will be mapped to columns with the same name.
+     *
+     * <p>Note: Natively supported types can be mapped to a single key/value column, otherwise table operation will ends up with exception.

Review comment:
       ```suggestion
        * <p>Note: Natively supported types can be mapped to a single key/value column, otherwise table operation will throw an exception.
   ```

##########
File path: modules/api/src/main/java/org/apache/ignite/table/mapper/Mapper.java
##########
@@ -17,61 +17,178 @@
 
 package org.apache.ignite.table.mapper;
 
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
+import java.lang.reflect.Modifier;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.BitSet;
+import java.util.UUID;
 
 /**
  * Mapper interface defines methods that are required for a marshaller to map class field names to table columns.
  *
- * @param <T> Mapped type.
+ * <p>NB: Anonymous, local, inner classes, and special types are NOT supported, and mapper will not be created for them: e.g. interfaces,
+ * annotation, and so on.
+ *
+ * @param <T> Type of which objects the mapper handles.
+ * @apiNote Implementation shouldn't use this interface directly, please, use {@link PojoMapper} or {@link OneColumnMapper}
+ *         instead.
+ * @see PojoMapper
+ * @see OneColumnMapper
  */
 public interface Mapper<T> {
     /**
-     * Creates a mapper for a class.
+     * Shortcut method creates a mapper for class. Natively supported types will be mapped to a single schema column, otherwise individual
+     * object fields will be mapped to columns with the same name.
+     *
+     * <p>Note: Natively supported types can be mapped to a single key/value column, otherwise table operation will ends up with exception.
+     * Use {@link #of(Class, String)} instead, to map to a concrete column name.
      *
-     * @param cls Key class.
-     * @param <K> Key type.
-     * @return Mapper.
+     * @param cls Target type.
+     * @return Mapper for key objects representing a single key column.
+     * @throws IllegalArgumentException If {@code cls} is of unsupported kind.
      */
-    static <K> Mapper<K> of(Class<K> cls) {
-        return identity(cls);
+    static <O> Mapper<O> of(Class<O> cls) {
+        if (nativelySupported(cls)) {
+            // TODO: Cache mappers (IGNITE-16094).
+            return new OneColumnMapperImpl<>(cls, null, null);
+        } else {
+            return builder(cls).automap().build();
+        }
     }
 
     /**
-     * Creates a mapper builder for a class.
+     * Creates a mapper for the case when an object represents an only one-column.

Review comment:
       ```suggestion
        * Creates a mapper for the case when an object represents only one column.
   ```

##########
File path: modules/api/src/main/java/org/apache/ignite/table/mapper/MapperBuilder.java
##########
@@ -26,9 +28,20 @@
 import org.jetbrains.annotations.NotNull;
 
 /**
- * Mapper builder.
+ * Mapper builder provides methods for mapping object fields to columns.
  *
- * @param <T> Mapped type.
+ * <p>By default, a user must explicitly map needed fields with columns
+ * in one-to-one manner using {@link #map} and/or {@link #map(String, String, TypeConverter)} methods, all missed columns and/or fields
+ * become unmapped, and will be ignored during further table operations.
+ *
+ * <p>Calling {@link #automap()} method changes default behavior, and maps all the missed fields to the

Review comment:
       ```suggestion
    * <p>Calling {@link #automap()} method maps all matching fields to columns by name. Any fields that don't match a column by name are ignored.
   ```

##########
File path: modules/api/src/main/java/org/apache/ignite/table/mapper/Mapper.java
##########
@@ -17,61 +17,178 @@
 
 package org.apache.ignite.table.mapper;
 
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
+import java.lang.reflect.Modifier;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.BitSet;
+import java.util.UUID;
 
 /**
  * Mapper interface defines methods that are required for a marshaller to map class field names to table columns.
  *
- * @param <T> Mapped type.
+ * <p>NB: Anonymous, local, inner classes, and special types are NOT supported, and mapper will not be created for them: e.g. interfaces,
+ * annotation, and so on.
+ *
+ * @param <T> Type of which objects the mapper handles.
+ * @apiNote Implementation shouldn't use this interface directly, please, use {@link PojoMapper} or {@link OneColumnMapper}
+ *         instead.
+ * @see PojoMapper
+ * @see OneColumnMapper
  */
 public interface Mapper<T> {
     /**
-     * Creates a mapper for a class.
+     * Shortcut method creates a mapper for class. Natively supported types will be mapped to a single schema column, otherwise individual
+     * object fields will be mapped to columns with the same name.
+     *
+     * <p>Note: Natively supported types can be mapped to a single key/value column, otherwise table operation will ends up with exception.
+     * Use {@link #of(Class, String)} instead, to map to a concrete column name.
      *
-     * @param cls Key class.
-     * @param <K> Key type.
-     * @return Mapper.
+     * @param cls Target type.
+     * @return Mapper for key objects representing a single key column.
+     * @throws IllegalArgumentException If {@code cls} is of unsupported kind.
      */
-    static <K> Mapper<K> of(Class<K> cls) {
-        return identity(cls);
+    static <O> Mapper<O> of(Class<O> cls) {
+        if (nativelySupported(cls)) {
+            // TODO: Cache mappers (IGNITE-16094).
+            return new OneColumnMapperImpl<>(cls, null, null);
+        } else {
+            return builder(cls).automap().build();
+        }
     }
 
     /**
-     * Creates a mapper builder for a class.
+     * Creates a mapper for the case when an object represents an only one-column.
+     *
+     * <p>The mapper can be used as key, value, or record mapper. However, single column record looks as degraded case.
+     *
+     * @param cls        Parametrized type of which objects the mapper will handle.
+     * @param columnName Column name to map object to.
+     * @return Mapper for objects representing a one column.
+     * @throws IllegalArgumentException If {@code cls} is of unsupported kind.
+     */
+    static <O> Mapper<O> of(Class<O> cls, String columnName) {
+        return new OneColumnMapperImpl<>(ensureNativelySupported(cls), columnName, null);
+    }
+
+    /**
+     * Creates a mapper for the case when an object represents an only one-column and additional transformation is required.

Review comment:
       ```suggestion
        * Creates a mapper for the case when an object represents only one column and additional transformation is required.
   ```

##########
File path: modules/api/src/main/java/org/apache/ignite/table/mapper/MapperBuilder.java
##########
@@ -37,93 +50,205 @@
     /** Column-to-field name mapping. */
     private Map<String, String> columnToFields;
 
+    /** Column converters. */
+    private Map<String, TypeConverter<?, ?>> columnConverters = new HashMap<>();
+
+    /** Column name for one-column mapping. */
+    private final String mappedToColumn;
+
+    /** Flag indicates, if all unmapped object fields should be mapped to the columns with same names automatically. */
+    private boolean automapFlag = false;
+
+    /** {@code True} if the {@link #build()} method was called, {@code false} otherwise. */
+    private boolean isStale;
+
     /**
-     * Creates a mapper builder for a type.
+     * Creates a mapper builder for a POJO type.
      *
      * @param targetType Target type.
      */
     MapperBuilder(@NotNull Class<T> targetType) {
-        this.targetType = targetType;
+        this.targetType = Mapper.ensureValidPojo(targetType);
 
+        mappedToColumn = null;
         columnToFields = new HashMap<>(targetType.getDeclaredFields().length);
     }
 
     /**
-     * Add mapping for a field to a column.
+     * Creates a mapper builder for a natively supported type.
+     *
+     * @param targetType   Target type.
+     * @param mappedColumn Column name to map to.
+     */
+    MapperBuilder(@NotNull Class<T> targetType, String mappedColumn) {
+        this.targetType = Mapper.ensureNativelySupported(targetType);
+
+        mappedToColumn = mappedColumn;
+        columnToFields = null;
+    }
+
+    /**
+     * Ensure this instance is valid.
      *
-     * @param fieldName  Field name.
-     * @param columnName Column name.
-     * @return {@code this} for chaining.
-     * @throws IllegalArgumentException if a column was already mapped to some field.
      * @throws IllegalStateException if tries to reuse the builder after a mapping has been built.
      */
-    public MapperBuilder<T> map(@NotNull String fieldName, @NotNull String columnName) {
-        if (columnToFields == null) {
+    private void ensureNotStale() {
+        if (isStale) {
             throw new IllegalStateException("Mapper builder can't be reused.");
         }
+    }
+
+    /**
+     * Ensure field name is valid and field with this name exists.
+     *
+     * @param fieldName Field name.
+     * @return Field name for chaining.
+     * @throws IllegalArgumentException If field is {@code null} or class has no declared field with given name.
+     */
+    private String requireValidField(String fieldName) {
+        try {
+            if (fieldName == null || targetType.getDeclaredField(fieldName) == null) {
+                throw new IllegalArgumentException("Mapping for a column already exists: " + fieldName);
+            }
+        } catch (NoSuchFieldException e) {
+            throw new IllegalArgumentException(
+                    String.format("Field not found for class: field=%s, class=%s", fieldName, targetType.getName()));
+        }
+
+        return fieldName;
+    }
+
+    /**
+     * Maps a field to a column.
+     *
+     * @param fieldName        Field name.
+     * @param columnName       Column name.
+     * @param fieldColumnPairs Vararg that accepts (fieldName, columnName) pairs.
+     * @return {@code this} for chaining.
+     * @throws IllegalArgumentException If a field name has not paired column name in {@code fieldColumnPairs}, or a column was already
+     *                                  mapped to another field.
+     * @throws IllegalStateException    if tries to reuse the builder after a mapping has been built.
+     */
+    public MapperBuilder<T> map(@NotNull String fieldName, @NotNull String columnName, String... fieldColumnPairs) {
+        ensureNotStale();
 
-        if (columnToFields.put(Objects.requireNonNull(columnName), Objects.requireNonNull(fieldName)) != null) {
+        if (columnToFields == null) {
+            throw new IllegalArgumentException("Natively supported types doesn't support field mapping.");
+        } else if (fieldColumnPairs.length % 2 != 0) {
+            throw new IllegalArgumentException("Missed a column name, which the field is mapped to.");

Review comment:
       ```suggestion
               throw new IllegalArgumentException("fieldColumnPairs length should be even.");
   ```

##########
File path: modules/api/src/main/java/org/apache/ignite/table/mapper/MapperBuilder.java
##########
@@ -37,93 +50,205 @@
     /** Column-to-field name mapping. */
     private Map<String, String> columnToFields;
 
+    /** Column converters. */
+    private Map<String, TypeConverter<?, ?>> columnConverters = new HashMap<>();
+
+    /** Column name for one-column mapping. */
+    private final String mappedToColumn;
+
+    /** Flag indicates, if all unmapped object fields should be mapped to the columns with same names automatically. */
+    private boolean automapFlag = false;
+
+    /** {@code True} if the {@link #build()} method was called, {@code false} otherwise. */
+    private boolean isStale;
+
     /**
-     * Creates a mapper builder for a type.
+     * Creates a mapper builder for a POJO type.
      *
      * @param targetType Target type.
      */
     MapperBuilder(@NotNull Class<T> targetType) {
-        this.targetType = targetType;
+        this.targetType = Mapper.ensureValidPojo(targetType);
 
+        mappedToColumn = null;
         columnToFields = new HashMap<>(targetType.getDeclaredFields().length);
     }
 
     /**
-     * Add mapping for a field to a column.
+     * Creates a mapper builder for a natively supported type.
+     *
+     * @param targetType   Target type.
+     * @param mappedColumn Column name to map to.
+     */
+    MapperBuilder(@NotNull Class<T> targetType, String mappedColumn) {
+        this.targetType = Mapper.ensureNativelySupported(targetType);
+
+        mappedToColumn = mappedColumn;
+        columnToFields = null;
+    }
+
+    /**
+     * Ensure this instance is valid.
      *
-     * @param fieldName  Field name.
-     * @param columnName Column name.
-     * @return {@code this} for chaining.
-     * @throws IllegalArgumentException if a column was already mapped to some field.
      * @throws IllegalStateException if tries to reuse the builder after a mapping has been built.
      */
-    public MapperBuilder<T> map(@NotNull String fieldName, @NotNull String columnName) {
-        if (columnToFields == null) {
+    private void ensureNotStale() {
+        if (isStale) {
             throw new IllegalStateException("Mapper builder can't be reused.");
         }
+    }
+
+    /**
+     * Ensure field name is valid and field with this name exists.
+     *
+     * @param fieldName Field name.
+     * @return Field name for chaining.
+     * @throws IllegalArgumentException If field is {@code null} or class has no declared field with given name.
+     */
+    private String requireValidField(String fieldName) {
+        try {
+            if (fieldName == null || targetType.getDeclaredField(fieldName) == null) {
+                throw new IllegalArgumentException("Mapping for a column already exists: " + fieldName);
+            }
+        } catch (NoSuchFieldException e) {
+            throw new IllegalArgumentException(
+                    String.format("Field not found for class: field=%s, class=%s", fieldName, targetType.getName()));
+        }
+
+        return fieldName;
+    }
+
+    /**
+     * Maps a field to a column.
+     *
+     * @param fieldName        Field name.
+     * @param columnName       Column name.
+     * @param fieldColumnPairs Vararg that accepts (fieldName, columnName) pairs.
+     * @return {@code this} for chaining.
+     * @throws IllegalArgumentException If a field name has not paired column name in {@code fieldColumnPairs}, or a column was already
+     *                                  mapped to another field.
+     * @throws IllegalStateException    if tries to reuse the builder after a mapping has been built.
+     */
+    public MapperBuilder<T> map(@NotNull String fieldName, @NotNull String columnName, String... fieldColumnPairs) {
+        ensureNotStale();
 
-        if (columnToFields.put(Objects.requireNonNull(columnName), Objects.requireNonNull(fieldName)) != null) {
+        if (columnToFields == null) {
+            throw new IllegalArgumentException("Natively supported types doesn't support field mapping.");
+        } else if (fieldColumnPairs.length % 2 != 0) {
+            throw new IllegalArgumentException("Missed a column name, which the field is mapped to.");
+        } else if (columnToFields.put(Objects.requireNonNull(columnName), requireValidField(fieldName)) != null) {
             throw new IllegalArgumentException("Mapping for a column already exists: " + columnName);
         }
 
+        for (int i = 0; i < fieldColumnPairs.length; i += 2) {
+            if (columnToFields.put(Objects.requireNonNull(fieldColumnPairs[i + 1]), requireValidField(fieldColumnPairs[i])) != null) {
+                throw new IllegalArgumentException("Mapping for a column already exists: " + columnName);
+            }
+        }
+
         return this;
     }
 
     /**
-     * Map a field to a type of given class.
+     * Maps a field to a column with using type converter. The value will be converted before write to and after read from column using

Review comment:
       ```suggestion
        * Maps a field to a column with a type converter. The value will be converted before write to and after read from column using
   ```

##########
File path: modules/api/src/main/java/org/apache/ignite/table/mapper/MapperBuilder.java
##########
@@ -26,9 +28,20 @@
 import org.jetbrains.annotations.NotNull;
 
 /**
- * Mapper builder.
+ * Mapper builder provides methods for mapping object fields to columns.
  *
- * @param <T> Mapped type.
+ * <p>By default, a user must explicitly map needed fields with columns

Review comment:
       ```suggestion
    * <p>By default, the user must explicitly map required fields to columns
   ```

##########
File path: modules/api/src/main/java/org/apache/ignite/table/mapper/Mapper.java
##########
@@ -17,61 +17,178 @@
 
 package org.apache.ignite.table.mapper;
 
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
+import java.lang.reflect.Modifier;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.BitSet;
+import java.util.UUID;
 
 /**
  * Mapper interface defines methods that are required for a marshaller to map class field names to table columns.
  *
- * @param <T> Mapped type.
+ * <p>NB: Anonymous, local, inner classes, and special types are NOT supported, and mapper will not be created for them: e.g. interfaces,
+ * annotation, and so on.
+ *
+ * @param <T> Type of which objects the mapper handles.
+ * @apiNote Implementation shouldn't use this interface directly, please, use {@link PojoMapper} or {@link OneColumnMapper}
+ *         instead.
+ * @see PojoMapper
+ * @see OneColumnMapper
  */
 public interface Mapper<T> {
     /**
-     * Creates a mapper for a class.
+     * Shortcut method creates a mapper for class. Natively supported types will be mapped to a single schema column, otherwise individual
+     * object fields will be mapped to columns with the same name.
+     *
+     * <p>Note: Natively supported types can be mapped to a single key/value column, otherwise table operation will ends up with exception.
+     * Use {@link #of(Class, String)} instead, to map to a concrete column name.
      *
-     * @param cls Key class.
-     * @param <K> Key type.
-     * @return Mapper.
+     * @param cls Target type.
+     * @return Mapper for key objects representing a single key column.
+     * @throws IllegalArgumentException If {@code cls} is of unsupported kind.
      */
-    static <K> Mapper<K> of(Class<K> cls) {
-        return identity(cls);
+    static <O> Mapper<O> of(Class<O> cls) {
+        if (nativelySupported(cls)) {
+            // TODO: Cache mappers (IGNITE-16094).
+            return new OneColumnMapperImpl<>(cls, null, null);
+        } else {
+            return builder(cls).automap().build();
+        }
     }
 
     /**
-     * Creates a mapper builder for a class.
+     * Creates a mapper for the case when an object represents an only one-column.
+     *
+     * <p>The mapper can be used as key, value, or record mapper. However, single column record looks as degraded case.
+     *
+     * @param cls        Parametrized type of which objects the mapper will handle.
+     * @param columnName Column name to map object to.
+     * @return Mapper for objects representing a one column.
+     * @throws IllegalArgumentException If {@code cls} is of unsupported kind.
+     */
+    static <O> Mapper<O> of(Class<O> cls, String columnName) {
+        return new OneColumnMapperImpl<>(ensureNativelySupported(cls), columnName, null);
+    }
+
+    /**
+     * Creates a mapper for the case when an object represents an only one-column and additional transformation is required.
+     *
+     * <p>The mapper can be used as key, value, or record mapper. However, single column record looks as degraded case.
      *
-     * @param cls Value class.
-     * @param <V> Value type.
+     * @param cls        Parametrized type of which objects the mapper will handle.
+     * @param columnName Column name to map object to.
+     * @param <ObjectT>  Mapper target type.
+     * @param <ColumnT>  MUST be a type, which compatible with the column type.
+     * @return Mapper for objects representing a one column.
+     * @throws IllegalArgumentException If {@code cls} is of unsupported kind.
+     */
+    static <ObjectT, ColumnT> Mapper<ObjectT> of(Class<ObjectT> cls, String columnName, TypeConverter<ObjectT, ColumnT> converter) {
+        return new OneColumnMapperImpl<>(cls, columnName, converter);
+    }
+
+    /**
+     * Shortcut method creates a mapper for a case when object individual fields map to the column by their names.
+     *
+     * <p>The mapper can be used as key, value, or record mapper.
+     *
+     * @param cls              Parametrized type of which objects the mapper will handle.
+     * @param fieldName        Object field name.
+     * @param columnName       Column name.
+     * @param fieldColumnPairs Vararg that accepts (fieldName, columnName) pairs.
+     * @return Mapper .
+     * @throws IllegalArgumentException If a field name has not paired column name in {@code fieldColumnPairs}, or {@code cls} is of
+     *                                  unsupported kind.
+     */
+    static <O> Mapper<O> of(Class<O> cls, String fieldName, String columnName, String... fieldColumnPairs) {
+        if (fieldColumnPairs.length % 2 != 0) {
+            throw new IllegalArgumentException(
+                    "Missed a column name, which the field is mapped to: " + fieldColumnPairs[fieldColumnPairs.length - 1]);
+        }
+
+        return builder(cls).map(fieldName, columnName, fieldColumnPairs).build();
+    }
+
+    /**
+     * Creates a mapper builder for objects of given class.
+     *
+     * <p>Note: Builder itself can't be reused.
+     *
+     * @param cls Parametrized type of which objects the mapper will handle. Class MUST have the default constructor,
      * @return Mapper builder.
+     * @throws IllegalArgumentException If {@code cls} is of unsupported kind.
      */
-    static <V> MapperBuilder<V> builderFor(Class<V> cls) {
-        return new MapperBuilder<>(cls);
+    static <O> MapperBuilder<O> builder(Class<O> cls) {
+        if (nativelySupported(cls)) {
+            return new MapperBuilder<>(cls, null);
+        } else {
+            return new MapperBuilder<>(cls);
+        }
     }
 
     /**
-     * Creates identity mapper which is used for simple types that have native support or objects with field names that match column names.
+     * Ensures class is of natively supported kind and can be used in one-column mapping.
      *
-     * @param targetClass Target type class.
-     * @param <T>         Target type.
-     * @return Mapper.
+     * @param cls Class to validate.
+     * @return {@code cls} if it is of natively supported kind.
+     * @throws IllegalArgumentException If {@code cls} is invalid and can't be used in one-column mapping.
      */
-    static <T> Mapper<T> identity(Class<T> targetClass) {
-        // TODO: Cache mappers (IGNITE-16094).
-        return new IdentityMapper<T>(targetClass);
+    static <O> Class<O> ensureNativelySupported(Class<O> cls) {
+        if (nativelySupported(cls)) {
+            return cls;
+        }
+
+        throw new IllegalArgumentException("Class has no native support (type converter required): " + cls.getName());
     }
 
     /**
-     * Return mapped type.
+     * Ensures class is of the supported kind for POJO mapping.
      *
-     * @return Mapped type.
+     * @param cls Class to validate.
+     * @return {@code cls} if it is valid POJO.
+     * @throws IllegalArgumentException If {@code cls} can't be used as POJO for mapping and/or of invalid kind.
      */
-    Class<T> targetType();
+    static <O> Class<O> ensureValidPojo(Class<O> cls) {
+        if (nativelySupported(cls) || cls.isAnonymousClass() || cls.isLocalClass() || cls.isSynthetic() || cls.isPrimitive() || cls.isEnum()
+                    || cls.isArray() || cls.isAnnotation() || (cls.isMemberClass() && !Modifier.isStatic(cls.getModifiers()))
+                    || Modifier.isAbstract(cls.getModifiers())) {
+            throw new IllegalArgumentException("Class is of unsupported kind: " + cls.getName());

Review comment:
       Can we make an exception more helpful and indicate what exactly is wrong with the class?
   E.g. `"Unsupported class. Anonymous types are not supported: " + cls.getName()`.

##########
File path: modules/api/src/main/java/org/apache/ignite/table/mapper/MapperBuilder.java
##########
@@ -37,93 +50,205 @@
     /** Column-to-field name mapping. */
     private Map<String, String> columnToFields;
 
+    /** Column converters. */
+    private Map<String, TypeConverter<?, ?>> columnConverters = new HashMap<>();
+
+    /** Column name for one-column mapping. */
+    private final String mappedToColumn;
+
+    /** Flag indicates, if all unmapped object fields should be mapped to the columns with same names automatically. */
+    private boolean automapFlag = false;
+
+    /** {@code True} if the {@link #build()} method was called, {@code false} otherwise. */
+    private boolean isStale;
+
     /**
-     * Creates a mapper builder for a type.
+     * Creates a mapper builder for a POJO type.
      *
      * @param targetType Target type.
      */
     MapperBuilder(@NotNull Class<T> targetType) {
-        this.targetType = targetType;
+        this.targetType = Mapper.ensureValidPojo(targetType);
 
+        mappedToColumn = null;
         columnToFields = new HashMap<>(targetType.getDeclaredFields().length);
     }
 
     /**
-     * Add mapping for a field to a column.
+     * Creates a mapper builder for a natively supported type.
+     *
+     * @param targetType   Target type.
+     * @param mappedColumn Column name to map to.
+     */
+    MapperBuilder(@NotNull Class<T> targetType, String mappedColumn) {
+        this.targetType = Mapper.ensureNativelySupported(targetType);
+
+        mappedToColumn = mappedColumn;
+        columnToFields = null;
+    }
+
+    /**
+     * Ensure this instance is valid.
      *
-     * @param fieldName  Field name.
-     * @param columnName Column name.
-     * @return {@code this} for chaining.
-     * @throws IllegalArgumentException if a column was already mapped to some field.
      * @throws IllegalStateException if tries to reuse the builder after a mapping has been built.
      */
-    public MapperBuilder<T> map(@NotNull String fieldName, @NotNull String columnName) {
-        if (columnToFields == null) {
+    private void ensureNotStale() {
+        if (isStale) {
             throw new IllegalStateException("Mapper builder can't be reused.");
         }
+    }
+
+    /**
+     * Ensure field name is valid and field with this name exists.
+     *
+     * @param fieldName Field name.
+     * @return Field name for chaining.
+     * @throws IllegalArgumentException If field is {@code null} or class has no declared field with given name.
+     */
+    private String requireValidField(String fieldName) {
+        try {
+            if (fieldName == null || targetType.getDeclaredField(fieldName) == null) {
+                throw new IllegalArgumentException("Mapping for a column already exists: " + fieldName);
+            }
+        } catch (NoSuchFieldException e) {
+            throw new IllegalArgumentException(
+                    String.format("Field not found for class: field=%s, class=%s", fieldName, targetType.getName()));
+        }
+
+        return fieldName;
+    }
+
+    /**
+     * Maps a field to a column.
+     *
+     * @param fieldName        Field name.
+     * @param columnName       Column name.
+     * @param fieldColumnPairs Vararg that accepts (fieldName, columnName) pairs.
+     * @return {@code this} for chaining.
+     * @throws IllegalArgumentException If a field name has not paired column name in {@code fieldColumnPairs}, or a column was already
+     *                                  mapped to another field.
+     * @throws IllegalStateException    if tries to reuse the builder after a mapping has been built.
+     */
+    public MapperBuilder<T> map(@NotNull String fieldName, @NotNull String columnName, String... fieldColumnPairs) {
+        ensureNotStale();
 
-        if (columnToFields.put(Objects.requireNonNull(columnName), Objects.requireNonNull(fieldName)) != null) {
+        if (columnToFields == null) {
+            throw new IllegalArgumentException("Natively supported types doesn't support field mapping.");
+        } else if (fieldColumnPairs.length % 2 != 0) {
+            throw new IllegalArgumentException("Missed a column name, which the field is mapped to.");
+        } else if (columnToFields.put(Objects.requireNonNull(columnName), requireValidField(fieldName)) != null) {
             throw new IllegalArgumentException("Mapping for a column already exists: " + columnName);
         }
 
+        for (int i = 0; i < fieldColumnPairs.length; i += 2) {
+            if (columnToFields.put(Objects.requireNonNull(fieldColumnPairs[i + 1]), requireValidField(fieldColumnPairs[i])) != null) {
+                throw new IllegalArgumentException("Mapping for a column already exists: " + columnName);

Review comment:
       Is there any disadvantage if we allow the user to overwrite previous mappings?

##########
File path: modules/api/src/main/java/org/apache/ignite/table/mapper/Mapper.java
##########
@@ -17,61 +17,178 @@
 
 package org.apache.ignite.table.mapper;
 
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
+import java.lang.reflect.Modifier;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.BitSet;
+import java.util.UUID;
 
 /**
  * Mapper interface defines methods that are required for a marshaller to map class field names to table columns.
  *
- * @param <T> Mapped type.
+ * <p>NB: Anonymous, local, inner classes, and special types are NOT supported, and mapper will not be created for them: e.g. interfaces,
+ * annotation, and so on.
+ *
+ * @param <T> Type of which objects the mapper handles.
+ * @apiNote Implementation shouldn't use this interface directly, please, use {@link PojoMapper} or {@link OneColumnMapper}
+ *         instead.
+ * @see PojoMapper
+ * @see OneColumnMapper
  */
 public interface Mapper<T> {
     /**
-     * Creates a mapper for a class.
+     * Shortcut method creates a mapper for class. Natively supported types will be mapped to a single schema column, otherwise individual
+     * object fields will be mapped to columns with the same name.
+     *
+     * <p>Note: Natively supported types can be mapped to a single key/value column, otherwise table operation will ends up with exception.
+     * Use {@link #of(Class, String)} instead, to map to a concrete column name.
      *
-     * @param cls Key class.
-     * @param <K> Key type.
-     * @return Mapper.
+     * @param cls Target type.
+     * @return Mapper for key objects representing a single key column.
+     * @throws IllegalArgumentException If {@code cls} is of unsupported kind.
      */
-    static <K> Mapper<K> of(Class<K> cls) {
-        return identity(cls);
+    static <O> Mapper<O> of(Class<O> cls) {
+        if (nativelySupported(cls)) {

Review comment:
       Here and below: I think we should check all user-provided arguments for `null`.

##########
File path: modules/api/src/main/java/org/apache/ignite/table/mapper/MapperBuilder.java
##########
@@ -37,93 +50,205 @@
     /** Column-to-field name mapping. */
     private Map<String, String> columnToFields;
 
+    /** Column converters. */
+    private Map<String, TypeConverter<?, ?>> columnConverters = new HashMap<>();
+
+    /** Column name for one-column mapping. */
+    private final String mappedToColumn;
+
+    /** Flag indicates, if all unmapped object fields should be mapped to the columns with same names automatically. */
+    private boolean automapFlag = false;
+
+    /** {@code True} if the {@link #build()} method was called, {@code false} otherwise. */
+    private boolean isStale;
+
     /**
-     * Creates a mapper builder for a type.
+     * Creates a mapper builder for a POJO type.
      *
      * @param targetType Target type.
      */
     MapperBuilder(@NotNull Class<T> targetType) {
-        this.targetType = targetType;
+        this.targetType = Mapper.ensureValidPojo(targetType);
 
+        mappedToColumn = null;
         columnToFields = new HashMap<>(targetType.getDeclaredFields().length);
     }
 
     /**
-     * Add mapping for a field to a column.
+     * Creates a mapper builder for a natively supported type.
+     *
+     * @param targetType   Target type.
+     * @param mappedColumn Column name to map to.
+     */
+    MapperBuilder(@NotNull Class<T> targetType, String mappedColumn) {
+        this.targetType = Mapper.ensureNativelySupported(targetType);
+
+        mappedToColumn = mappedColumn;
+        columnToFields = null;
+    }
+
+    /**
+     * Ensure this instance is valid.
      *
-     * @param fieldName  Field name.
-     * @param columnName Column name.
-     * @return {@code this} for chaining.
-     * @throws IllegalArgumentException if a column was already mapped to some field.
      * @throws IllegalStateException if tries to reuse the builder after a mapping has been built.
      */
-    public MapperBuilder<T> map(@NotNull String fieldName, @NotNull String columnName) {
-        if (columnToFields == null) {
+    private void ensureNotStale() {
+        if (isStale) {
             throw new IllegalStateException("Mapper builder can't be reused.");
         }
+    }
+
+    /**
+     * Ensure field name is valid and field with this name exists.
+     *
+     * @param fieldName Field name.
+     * @return Field name for chaining.
+     * @throws IllegalArgumentException If field is {@code null} or class has no declared field with given name.
+     */
+    private String requireValidField(String fieldName) {
+        try {
+            if (fieldName == null || targetType.getDeclaredField(fieldName) == null) {
+                throw new IllegalArgumentException("Mapping for a column already exists: " + fieldName);
+            }
+        } catch (NoSuchFieldException e) {
+            throw new IllegalArgumentException(
+                    String.format("Field not found for class: field=%s, class=%s", fieldName, targetType.getName()));
+        }
+
+        return fieldName;
+    }
+
+    /**
+     * Maps a field to a column.
+     *
+     * @param fieldName        Field name.
+     * @param columnName       Column name.
+     * @param fieldColumnPairs Vararg that accepts (fieldName, columnName) pairs.
+     * @return {@code this} for chaining.
+     * @throws IllegalArgumentException If a field name has not paired column name in {@code fieldColumnPairs}, or a column was already
+     *                                  mapped to another field.
+     * @throws IllegalStateException    if tries to reuse the builder after a mapping has been built.
+     */
+    public MapperBuilder<T> map(@NotNull String fieldName, @NotNull String columnName, String... fieldColumnPairs) {
+        ensureNotStale();
 
-        if (columnToFields.put(Objects.requireNonNull(columnName), Objects.requireNonNull(fieldName)) != null) {
+        if (columnToFields == null) {
+            throw new IllegalArgumentException("Natively supported types doesn't support field mapping.");
+        } else if (fieldColumnPairs.length % 2 != 0) {
+            throw new IllegalArgumentException("Missed a column name, which the field is mapped to.");
+        } else if (columnToFields.put(Objects.requireNonNull(columnName), requireValidField(fieldName)) != null) {
             throw new IllegalArgumentException("Mapping for a column already exists: " + columnName);
         }
 
+        for (int i = 0; i < fieldColumnPairs.length; i += 2) {
+            if (columnToFields.put(Objects.requireNonNull(fieldColumnPairs[i + 1]), requireValidField(fieldColumnPairs[i])) != null) {
+                throw new IllegalArgumentException("Mapping for a column already exists: " + columnName);
+            }
+        }
+
         return this;
     }
 
     /**
-     * Map a field to a type of given class.
+     * Maps a field to a column with using type converter. The value will be converted before write to and after read from column using
+     * provided converter.
      *
-     * @param fieldName   Field name.
-     * @param targetClass Target class.
-     * @return {@code this} for chaining.
+     * @param <ObjectT>  MUST match the object field type, if the individual field mapped to given column.
+     * @param <ColumnT>  MUST be a type, which compatible with the column type.

Review comment:
       ```suggestion
        * @param <ColumnT> Column type. Must be assignable to the table column type.
   ```

##########
File path: modules/api/src/main/java/org/apache/ignite/table/mapper/MapperBuilder.java
##########
@@ -37,93 +50,205 @@
     /** Column-to-field name mapping. */
     private Map<String, String> columnToFields;
 
+    /** Column converters. */
+    private Map<String, TypeConverter<?, ?>> columnConverters = new HashMap<>();
+
+    /** Column name for one-column mapping. */
+    private final String mappedToColumn;
+
+    /** Flag indicates, if all unmapped object fields should be mapped to the columns with same names automatically. */
+    private boolean automapFlag = false;
+
+    /** {@code True} if the {@link #build()} method was called, {@code false} otherwise. */
+    private boolean isStale;
+
     /**
-     * Creates a mapper builder for a type.
+     * Creates a mapper builder for a POJO type.
      *
      * @param targetType Target type.
      */
     MapperBuilder(@NotNull Class<T> targetType) {
-        this.targetType = targetType;
+        this.targetType = Mapper.ensureValidPojo(targetType);
 
+        mappedToColumn = null;
         columnToFields = new HashMap<>(targetType.getDeclaredFields().length);
     }
 
     /**
-     * Add mapping for a field to a column.
+     * Creates a mapper builder for a natively supported type.
+     *
+     * @param targetType   Target type.
+     * @param mappedColumn Column name to map to.
+     */
+    MapperBuilder(@NotNull Class<T> targetType, String mappedColumn) {
+        this.targetType = Mapper.ensureNativelySupported(targetType);
+
+        mappedToColumn = mappedColumn;
+        columnToFields = null;
+    }
+
+    /**
+     * Ensure this instance is valid.
      *
-     * @param fieldName  Field name.
-     * @param columnName Column name.
-     * @return {@code this} for chaining.
-     * @throws IllegalArgumentException if a column was already mapped to some field.
      * @throws IllegalStateException if tries to reuse the builder after a mapping has been built.
      */
-    public MapperBuilder<T> map(@NotNull String fieldName, @NotNull String columnName) {
-        if (columnToFields == null) {
+    private void ensureNotStale() {
+        if (isStale) {
             throw new IllegalStateException("Mapper builder can't be reused.");
         }
+    }
+
+    /**
+     * Ensure field name is valid and field with this name exists.
+     *
+     * @param fieldName Field name.
+     * @return Field name for chaining.
+     * @throws IllegalArgumentException If field is {@code null} or class has no declared field with given name.
+     */
+    private String requireValidField(String fieldName) {
+        try {
+            if (fieldName == null || targetType.getDeclaredField(fieldName) == null) {
+                throw new IllegalArgumentException("Mapping for a column already exists: " + fieldName);
+            }
+        } catch (NoSuchFieldException e) {
+            throw new IllegalArgumentException(
+                    String.format("Field not found for class: field=%s, class=%s", fieldName, targetType.getName()));
+        }
+
+        return fieldName;
+    }
+
+    /**
+     * Maps a field to a column.
+     *
+     * @param fieldName        Field name.
+     * @param columnName       Column name.
+     * @param fieldColumnPairs Vararg that accepts (fieldName, columnName) pairs.
+     * @return {@code this} for chaining.
+     * @throws IllegalArgumentException If a field name has not paired column name in {@code fieldColumnPairs}, or a column was already
+     *                                  mapped to another field.
+     * @throws IllegalStateException    if tries to reuse the builder after a mapping has been built.
+     */
+    public MapperBuilder<T> map(@NotNull String fieldName, @NotNull String columnName, String... fieldColumnPairs) {
+        ensureNotStale();
 
-        if (columnToFields.put(Objects.requireNonNull(columnName), Objects.requireNonNull(fieldName)) != null) {
+        if (columnToFields == null) {
+            throw new IllegalArgumentException("Natively supported types doesn't support field mapping.");
+        } else if (fieldColumnPairs.length % 2 != 0) {
+            throw new IllegalArgumentException("Missed a column name, which the field is mapped to.");
+        } else if (columnToFields.put(Objects.requireNonNull(columnName), requireValidField(fieldName)) != null) {
             throw new IllegalArgumentException("Mapping for a column already exists: " + columnName);
         }
 
+        for (int i = 0; i < fieldColumnPairs.length; i += 2) {
+            if (columnToFields.put(Objects.requireNonNull(fieldColumnPairs[i + 1]), requireValidField(fieldColumnPairs[i])) != null) {
+                throw new IllegalArgumentException("Mapping for a column already exists: " + columnName);
+            }
+        }
+
         return this;
     }
 
     /**
-     * Map a field to a type of given class.
+     * Maps a field to a column with using type converter. The value will be converted before write to and after read from column using
+     * provided converter.
      *
-     * @param fieldName   Field name.
-     * @param targetClass Target class.
-     * @return {@code this} for chaining.
+     * @param <ObjectT>  MUST match the object field type, if the individual field mapped to given column.
+     * @param <ColumnT>  MUST be a type, which compatible with the column type.
+     * @param fieldName  Field name.
+     * @param columnName Column name.
+     * @param converter  Converter for objects of {@link ColumnT} and {@link ObjectT}.
      */
-    public MapperBuilder<T> map(@NotNull String fieldName, Class<?> targetClass) {
-        throw new UnsupportedOperationException("Not implemented yet.");
+    public <ObjectT, ColumnT> MapperBuilder<T> map(
+            @NotNull String fieldName,
+            @NotNull String columnName,
+            @NotNull TypeConverter<ObjectT, ColumnT> converter
+    ) {
+        map(fieldName, columnName);
+        convert(converter, columnName);
+
+        return this;
     }
 
     /**
-     * Adds a functional mapping for a field, the result depends on function call for every particular row.
+     * Adds a manual functional mapping for an object and row represented by tuple.
      *
-     * @param fieldName       Field name.
-     * @param mappingFunction Mapper function.
+     * @param objectToRow Object to tuple function.
+     * @param rowToObject Tuple to object function.
      * @return {@code this} for chaining.
      */
-    public MapperBuilder<T> map(@NotNull String fieldName, Function<Tuple, Object> mappingFunction) {
+    public MapperBuilder<T> map(Function<T, Tuple> objectToRow, Function<Tuple, T> rowToObject) {
+        ensureNotStale();
+
         throw new UnsupportedOperationException("Not implemented yet.");
     }
 
     /**
-     * Sets a target class to deserialize to.
+     * Sets a converter for a column, which value must be converted before write/after read.
+     *
+     * @param converter  Converter for objects of {@link ColumnT} and {@link ObjectT}.
+     * @param columnName Column name.
+     * @param <ObjectT>  MUST match the object field type, if a field mapped to given column, or the object type {@link T}
+     * @param <ColumnT>  MUST be a type, which compatible with the column type.

Review comment:
       Same as above.

##########
File path: modules/api/src/main/java/org/apache/ignite/table/mapper/MapperBuilder.java
##########
@@ -37,93 +50,205 @@
     /** Column-to-field name mapping. */
     private Map<String, String> columnToFields;
 
+    /** Column converters. */
+    private Map<String, TypeConverter<?, ?>> columnConverters = new HashMap<>();
+
+    /** Column name for one-column mapping. */
+    private final String mappedToColumn;
+
+    /** Flag indicates, if all unmapped object fields should be mapped to the columns with same names automatically. */
+    private boolean automapFlag = false;
+
+    /** {@code True} if the {@link #build()} method was called, {@code false} otherwise. */
+    private boolean isStale;
+
     /**
-     * Creates a mapper builder for a type.
+     * Creates a mapper builder for a POJO type.
      *
      * @param targetType Target type.
      */
     MapperBuilder(@NotNull Class<T> targetType) {
-        this.targetType = targetType;
+        this.targetType = Mapper.ensureValidPojo(targetType);
 
+        mappedToColumn = null;
         columnToFields = new HashMap<>(targetType.getDeclaredFields().length);
     }
 
     /**
-     * Add mapping for a field to a column.
+     * Creates a mapper builder for a natively supported type.
+     *
+     * @param targetType   Target type.
+     * @param mappedColumn Column name to map to.
+     */
+    MapperBuilder(@NotNull Class<T> targetType, String mappedColumn) {
+        this.targetType = Mapper.ensureNativelySupported(targetType);
+
+        mappedToColumn = mappedColumn;
+        columnToFields = null;
+    }
+
+    /**
+     * Ensure this instance is valid.
      *
-     * @param fieldName  Field name.
-     * @param columnName Column name.
-     * @return {@code this} for chaining.
-     * @throws IllegalArgumentException if a column was already mapped to some field.
      * @throws IllegalStateException if tries to reuse the builder after a mapping has been built.
      */
-    public MapperBuilder<T> map(@NotNull String fieldName, @NotNull String columnName) {
-        if (columnToFields == null) {
+    private void ensureNotStale() {
+        if (isStale) {
             throw new IllegalStateException("Mapper builder can't be reused.");
         }
+    }
+
+    /**
+     * Ensure field name is valid and field with this name exists.
+     *
+     * @param fieldName Field name.
+     * @return Field name for chaining.
+     * @throws IllegalArgumentException If field is {@code null} or class has no declared field with given name.
+     */
+    private String requireValidField(String fieldName) {
+        try {
+            if (fieldName == null || targetType.getDeclaredField(fieldName) == null) {
+                throw new IllegalArgumentException("Mapping for a column already exists: " + fieldName);
+            }
+        } catch (NoSuchFieldException e) {
+            throw new IllegalArgumentException(
+                    String.format("Field not found for class: field=%s, class=%s", fieldName, targetType.getName()));
+        }
+
+        return fieldName;
+    }
+
+    /**
+     * Maps a field to a column.
+     *
+     * @param fieldName        Field name.
+     * @param columnName       Column name.
+     * @param fieldColumnPairs Vararg that accepts (fieldName, columnName) pairs.
+     * @return {@code this} for chaining.
+     * @throws IllegalArgumentException If a field name has not paired column name in {@code fieldColumnPairs}, or a column was already
+     *                                  mapped to another field.
+     * @throws IllegalStateException    if tries to reuse the builder after a mapping has been built.
+     */
+    public MapperBuilder<T> map(@NotNull String fieldName, @NotNull String columnName, String... fieldColumnPairs) {
+        ensureNotStale();
 
-        if (columnToFields.put(Objects.requireNonNull(columnName), Objects.requireNonNull(fieldName)) != null) {
+        if (columnToFields == null) {
+            throw new IllegalArgumentException("Natively supported types doesn't support field mapping.");
+        } else if (fieldColumnPairs.length % 2 != 0) {
+            throw new IllegalArgumentException("Missed a column name, which the field is mapped to.");
+        } else if (columnToFields.put(Objects.requireNonNull(columnName), requireValidField(fieldName)) != null) {
             throw new IllegalArgumentException("Mapping for a column already exists: " + columnName);
         }
 
+        for (int i = 0; i < fieldColumnPairs.length; i += 2) {
+            if (columnToFields.put(Objects.requireNonNull(fieldColumnPairs[i + 1]), requireValidField(fieldColumnPairs[i])) != null) {
+                throw new IllegalArgumentException("Mapping for a column already exists: " + columnName);
+            }
+        }
+
         return this;
     }
 
     /**
-     * Map a field to a type of given class.
+     * Maps a field to a column with using type converter. The value will be converted before write to and after read from column using
+     * provided converter.
      *
-     * @param fieldName   Field name.
-     * @param targetClass Target class.
-     * @return {@code this} for chaining.
+     * @param <ObjectT>  MUST match the object field type, if the individual field mapped to given column.
+     * @param <ColumnT>  MUST be a type, which compatible with the column type.
+     * @param fieldName  Field name.
+     * @param columnName Column name.
+     * @param converter  Converter for objects of {@link ColumnT} and {@link ObjectT}.
      */
-    public MapperBuilder<T> map(@NotNull String fieldName, Class<?> targetClass) {
-        throw new UnsupportedOperationException("Not implemented yet.");
+    public <ObjectT, ColumnT> MapperBuilder<T> map(
+            @NotNull String fieldName,
+            @NotNull String columnName,
+            @NotNull TypeConverter<ObjectT, ColumnT> converter
+    ) {
+        map(fieldName, columnName);
+        convert(converter, columnName);
+
+        return this;
     }
 
     /**
-     * Adds a functional mapping for a field, the result depends on function call for every particular row.
+     * Adds a manual functional mapping for an object and row represented by tuple.
      *
-     * @param fieldName       Field name.
-     * @param mappingFunction Mapper function.
+     * @param objectToRow Object to tuple function.
+     * @param rowToObject Tuple to object function.
      * @return {@code this} for chaining.
      */
-    public MapperBuilder<T> map(@NotNull String fieldName, Function<Tuple, Object> mappingFunction) {
+    public MapperBuilder<T> map(Function<T, Tuple> objectToRow, Function<Tuple, T> rowToObject) {
+        ensureNotStale();
+
         throw new UnsupportedOperationException("Not implemented yet.");
     }
 
     /**
-     * Sets a target class to deserialize to.
+     * Sets a converter for a column, which value must be converted before write/after read.
+     *
+     * @param converter  Converter for objects of {@link ColumnT} and {@link ObjectT}.
+     * @param columnName Column name.
+     * @param <ObjectT>  MUST match the object field type, if a field mapped to given column, or the object type {@link T}
+     * @param <ColumnT>  MUST be a type, which compatible with the column type.
+     */
+    public <ObjectT, ColumnT> MapperBuilder<T> convert(
+            @NotNull TypeConverter<ObjectT, ColumnT> converter,
+            @NotNull String columnName
+    ) {
+        ensureNotStale();
+
+        if (columnConverters.put(columnName, converter) != null) {
+            throw new IllegalArgumentException("Column converter already exists: " + columnName);
+        }
+
+        return this;
+    }
+
+    /**
+     * Make mapper treat missed columns as they mapped to the field of the same name. If class {@link T} has no field for the missed column,

Review comment:
       ```suggestion
        * Maps all matching fields to columns by name. Any fields that don't match a column by name are ignored.
   ```

##########
File path: modules/api/src/main/java/org/apache/ignite/table/mapper/MapperBuilder.java
##########
@@ -37,93 +50,205 @@
     /** Column-to-field name mapping. */
     private Map<String, String> columnToFields;
 
+    /** Column converters. */
+    private Map<String, TypeConverter<?, ?>> columnConverters = new HashMap<>();
+
+    /** Column name for one-column mapping. */
+    private final String mappedToColumn;
+
+    /** Flag indicates, if all unmapped object fields should be mapped to the columns with same names automatically. */
+    private boolean automapFlag = false;
+
+    /** {@code True} if the {@link #build()} method was called, {@code false} otherwise. */
+    private boolean isStale;
+
     /**
-     * Creates a mapper builder for a type.
+     * Creates a mapper builder for a POJO type.
      *
      * @param targetType Target type.
      */
     MapperBuilder(@NotNull Class<T> targetType) {
-        this.targetType = targetType;
+        this.targetType = Mapper.ensureValidPojo(targetType);
 
+        mappedToColumn = null;
         columnToFields = new HashMap<>(targetType.getDeclaredFields().length);
     }
 
     /**
-     * Add mapping for a field to a column.
+     * Creates a mapper builder for a natively supported type.
+     *
+     * @param targetType   Target type.
+     * @param mappedColumn Column name to map to.
+     */
+    MapperBuilder(@NotNull Class<T> targetType, String mappedColumn) {
+        this.targetType = Mapper.ensureNativelySupported(targetType);
+
+        mappedToColumn = mappedColumn;
+        columnToFields = null;
+    }
+
+    /**
+     * Ensure this instance is valid.
      *
-     * @param fieldName  Field name.
-     * @param columnName Column name.
-     * @return {@code this} for chaining.
-     * @throws IllegalArgumentException if a column was already mapped to some field.
      * @throws IllegalStateException if tries to reuse the builder after a mapping has been built.
      */
-    public MapperBuilder<T> map(@NotNull String fieldName, @NotNull String columnName) {
-        if (columnToFields == null) {
+    private void ensureNotStale() {
+        if (isStale) {
             throw new IllegalStateException("Mapper builder can't be reused.");
         }
+    }
+
+    /**
+     * Ensure field name is valid and field with this name exists.
+     *
+     * @param fieldName Field name.
+     * @return Field name for chaining.
+     * @throws IllegalArgumentException If field is {@code null} or class has no declared field with given name.
+     */
+    private String requireValidField(String fieldName) {
+        try {
+            if (fieldName == null || targetType.getDeclaredField(fieldName) == null) {
+                throw new IllegalArgumentException("Mapping for a column already exists: " + fieldName);
+            }
+        } catch (NoSuchFieldException e) {
+            throw new IllegalArgumentException(
+                    String.format("Field not found for class: field=%s, class=%s", fieldName, targetType.getName()));
+        }
+
+        return fieldName;
+    }
+
+    /**
+     * Maps a field to a column.
+     *
+     * @param fieldName        Field name.
+     * @param columnName       Column name.
+     * @param fieldColumnPairs Vararg that accepts (fieldName, columnName) pairs.
+     * @return {@code this} for chaining.
+     * @throws IllegalArgumentException If a field name has not paired column name in {@code fieldColumnPairs}, or a column was already
+     *                                  mapped to another field.
+     * @throws IllegalStateException    if tries to reuse the builder after a mapping has been built.
+     */
+    public MapperBuilder<T> map(@NotNull String fieldName, @NotNull String columnName, String... fieldColumnPairs) {
+        ensureNotStale();
 
-        if (columnToFields.put(Objects.requireNonNull(columnName), Objects.requireNonNull(fieldName)) != null) {
+        if (columnToFields == null) {
+            throw new IllegalArgumentException("Natively supported types doesn't support field mapping.");
+        } else if (fieldColumnPairs.length % 2 != 0) {
+            throw new IllegalArgumentException("Missed a column name, which the field is mapped to.");
+        } else if (columnToFields.put(Objects.requireNonNull(columnName), requireValidField(fieldName)) != null) {
             throw new IllegalArgumentException("Mapping for a column already exists: " + columnName);
         }
 
+        for (int i = 0; i < fieldColumnPairs.length; i += 2) {
+            if (columnToFields.put(Objects.requireNonNull(fieldColumnPairs[i + 1]), requireValidField(fieldColumnPairs[i])) != null) {
+                throw new IllegalArgumentException("Mapping for a column already exists: " + columnName);
+            }
+        }
+
         return this;
     }
 
     /**
-     * Map a field to a type of given class.
+     * Maps a field to a column with using type converter. The value will be converted before write to and after read from column using
+     * provided converter.
      *
-     * @param fieldName   Field name.
-     * @param targetClass Target class.
-     * @return {@code this} for chaining.
+     * @param <ObjectT>  MUST match the object field type, if the individual field mapped to given column.

Review comment:
       Value type. Must match the object field type if the individual field is mapped to a given column.

##########
File path: modules/api/src/main/java/org/apache/ignite/table/mapper/OneColumnMapper.java
##########
@@ -0,0 +1,47 @@
+/*
+ * 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.ignite.table.mapper;
+
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Maps whole object of natively supported type to a one column. Used to map object itself, but not the object fields.
+ *
+ * @apiNote When mapper is used for mapping key/value objects, and the schema contains single key/value column, it is possible to
+ *         map object without specifying a column name. However, if there are more columns and no concrete one was specified, then table
+ *         operations with using such mapper may fail with a schema mismatch exception due to ambiguity.
+ */
+public interface OneColumnMapper<ObjectT> extends Mapper<ObjectT> {
+    /**
+     * Returns a column name the object is mapped to, or {@code null} if not specified. If column name wasn't specified, mapper will to map

Review comment:
       ```suggestion
        * Returns a column name the object is mapped to, or {@code null} if not specified. If column name wasn't specified, the mapper maps the entire
   ```

##########
File path: modules/api/src/main/java/org/apache/ignite/table/mapper/MapperBuilder.java
##########
@@ -37,93 +50,205 @@
     /** Column-to-field name mapping. */
     private Map<String, String> columnToFields;
 
+    /** Column converters. */
+    private Map<String, TypeConverter<?, ?>> columnConverters = new HashMap<>();
+
+    /** Column name for one-column mapping. */
+    private final String mappedToColumn;
+
+    /** Flag indicates, if all unmapped object fields should be mapped to the columns with same names automatically. */
+    private boolean automapFlag = false;
+
+    /** {@code True} if the {@link #build()} method was called, {@code false} otherwise. */
+    private boolean isStale;
+
     /**
-     * Creates a mapper builder for a type.
+     * Creates a mapper builder for a POJO type.
      *
      * @param targetType Target type.
      */
     MapperBuilder(@NotNull Class<T> targetType) {
-        this.targetType = targetType;
+        this.targetType = Mapper.ensureValidPojo(targetType);
 
+        mappedToColumn = null;
         columnToFields = new HashMap<>(targetType.getDeclaredFields().length);
     }
 
     /**
-     * Add mapping for a field to a column.
+     * Creates a mapper builder for a natively supported type.
+     *
+     * @param targetType   Target type.
+     * @param mappedColumn Column name to map to.
+     */
+    MapperBuilder(@NotNull Class<T> targetType, String mappedColumn) {
+        this.targetType = Mapper.ensureNativelySupported(targetType);
+
+        mappedToColumn = mappedColumn;
+        columnToFields = null;
+    }
+
+    /**
+     * Ensure this instance is valid.
      *
-     * @param fieldName  Field name.
-     * @param columnName Column name.
-     * @return {@code this} for chaining.
-     * @throws IllegalArgumentException if a column was already mapped to some field.
      * @throws IllegalStateException if tries to reuse the builder after a mapping has been built.
      */
-    public MapperBuilder<T> map(@NotNull String fieldName, @NotNull String columnName) {
-        if (columnToFields == null) {
+    private void ensureNotStale() {
+        if (isStale) {
             throw new IllegalStateException("Mapper builder can't be reused.");
         }
+    }
+
+    /**
+     * Ensure field name is valid and field with this name exists.
+     *
+     * @param fieldName Field name.
+     * @return Field name for chaining.
+     * @throws IllegalArgumentException If field is {@code null} or class has no declared field with given name.
+     */
+    private String requireValidField(String fieldName) {
+        try {
+            if (fieldName == null || targetType.getDeclaredField(fieldName) == null) {
+                throw new IllegalArgumentException("Mapping for a column already exists: " + fieldName);
+            }
+        } catch (NoSuchFieldException e) {
+            throw new IllegalArgumentException(
+                    String.format("Field not found for class: field=%s, class=%s", fieldName, targetType.getName()));
+        }
+
+        return fieldName;
+    }
+
+    /**
+     * Maps a field to a column.
+     *
+     * @param fieldName        Field name.
+     * @param columnName       Column name.
+     * @param fieldColumnPairs Vararg that accepts (fieldName, columnName) pairs.
+     * @return {@code this} for chaining.
+     * @throws IllegalArgumentException If a field name has not paired column name in {@code fieldColumnPairs}, or a column was already
+     *                                  mapped to another field.
+     * @throws IllegalStateException    if tries to reuse the builder after a mapping has been built.
+     */
+    public MapperBuilder<T> map(@NotNull String fieldName, @NotNull String columnName, String... fieldColumnPairs) {
+        ensureNotStale();
 
-        if (columnToFields.put(Objects.requireNonNull(columnName), Objects.requireNonNull(fieldName)) != null) {
+        if (columnToFields == null) {
+            throw new IllegalArgumentException("Natively supported types doesn't support field mapping.");
+        } else if (fieldColumnPairs.length % 2 != 0) {
+            throw new IllegalArgumentException("Missed a column name, which the field is mapped to.");
+        } else if (columnToFields.put(Objects.requireNonNull(columnName), requireValidField(fieldName)) != null) {
             throw new IllegalArgumentException("Mapping for a column already exists: " + columnName);
         }
 
+        for (int i = 0; i < fieldColumnPairs.length; i += 2) {
+            if (columnToFields.put(Objects.requireNonNull(fieldColumnPairs[i + 1]), requireValidField(fieldColumnPairs[i])) != null) {
+                throw new IllegalArgumentException("Mapping for a column already exists: " + columnName);
+            }
+        }
+
         return this;
     }
 
     /**
-     * Map a field to a type of given class.
+     * Maps a field to a column with using type converter. The value will be converted before write to and after read from column using
+     * provided converter.
      *
-     * @param fieldName   Field name.
-     * @param targetClass Target class.
-     * @return {@code this} for chaining.
+     * @param <ObjectT>  MUST match the object field type, if the individual field mapped to given column.
+     * @param <ColumnT>  MUST be a type, which compatible with the column type.
+     * @param fieldName  Field name.
+     * @param columnName Column name.
+     * @param converter  Converter for objects of {@link ColumnT} and {@link ObjectT}.
      */
-    public MapperBuilder<T> map(@NotNull String fieldName, Class<?> targetClass) {
-        throw new UnsupportedOperationException("Not implemented yet.");
+    public <ObjectT, ColumnT> MapperBuilder<T> map(
+            @NotNull String fieldName,
+            @NotNull String columnName,
+            @NotNull TypeConverter<ObjectT, ColumnT> converter
+    ) {
+        map(fieldName, columnName);
+        convert(converter, columnName);
+
+        return this;
     }
 
     /**
-     * Adds a functional mapping for a field, the result depends on function call for every particular row.
+     * Adds a manual functional mapping for an object and row represented by tuple.
      *
-     * @param fieldName       Field name.
-     * @param mappingFunction Mapper function.
+     * @param objectToRow Object to tuple function.
+     * @param rowToObject Tuple to object function.
      * @return {@code this} for chaining.
      */
-    public MapperBuilder<T> map(@NotNull String fieldName, Function<Tuple, Object> mappingFunction) {
+    public MapperBuilder<T> map(Function<T, Tuple> objectToRow, Function<Tuple, T> rowToObject) {
+        ensureNotStale();
+
         throw new UnsupportedOperationException("Not implemented yet.");
     }
 
     /**
-     * Sets a target class to deserialize to.
+     * Sets a converter for a column, which value must be converted before write/after read.
+     *
+     * @param converter  Converter for objects of {@link ColumnT} and {@link ObjectT}.
+     * @param columnName Column name.
+     * @param <ObjectT>  MUST match the object field type, if a field mapped to given column, or the object type {@link T}
+     * @param <ColumnT>  MUST be a type, which compatible with the column type.
+     */
+    public <ObjectT, ColumnT> MapperBuilder<T> convert(
+            @NotNull TypeConverter<ObjectT, ColumnT> converter,

Review comment:
       Should the column name come first?

##########
File path: modules/api/src/main/java/org/apache/ignite/table/mapper/OneColumnMapper.java
##########
@@ -0,0 +1,47 @@
+/*
+ * 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.ignite.table.mapper;
+
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Maps whole object of natively supported type to a one column. Used to map object itself, but not the object fields.

Review comment:
       ```suggestion
    * Maps the whole object of natively supported type to a single column. Used to map the object itself, but not the object fields.
   ```

##########
File path: modules/api/src/main/java/org/apache/ignite/table/mapper/Mapper.java
##########
@@ -17,61 +17,178 @@
 
 package org.apache.ignite.table.mapper;
 
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
+import java.lang.reflect.Modifier;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.BitSet;
+import java.util.UUID;
 
 /**
  * Mapper interface defines methods that are required for a marshaller to map class field names to table columns.
  *
- * @param <T> Mapped type.
+ * <p>NB: Anonymous, local, inner classes, and special types are NOT supported, and mapper will not be created for them: e.g. interfaces,
+ * annotation, and so on.
+ *
+ * @param <T> Type of which objects the mapper handles.
+ * @apiNote Implementation shouldn't use this interface directly, please, use {@link PojoMapper} or {@link OneColumnMapper}
+ *         instead.
+ * @see PojoMapper
+ * @see OneColumnMapper
  */
 public interface Mapper<T> {
     /**
-     * Creates a mapper for a class.
+     * Shortcut method creates a mapper for class. Natively supported types will be mapped to a single schema column, otherwise individual
+     * object fields will be mapped to columns with the same name.
+     *
+     * <p>Note: Natively supported types can be mapped to a single key/value column, otherwise table operation will ends up with exception.

Review comment:
       > otherwise table operation will throw an exception
   
   It is not clear in which case an exception will be thrown:
   - When a type is not supported natively
   - When there is more than 1 column
   




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [ignite-3] ptupitsyn commented on a change in pull request #455: IGNITE-15784 Mapper support arbitrary serializable user pojo.

Posted by GitBox <gi...@apache.org>.
ptupitsyn commented on a change in pull request #455:
URL: https://github.com/apache/ignite-3/pull/455#discussion_r767745590



##########
File path: modules/api/src/main/java/org/apache/ignite/table/mapper/PojoMapper.java
##########
@@ -0,0 +1,46 @@
+/*
+ * 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.ignite.table.mapper;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Mapper implementation which maps fields of objects of type {@link T} to the columns by their names. Intended to use only with natively

Review comment:
       > Intended to use only with natively supported types
   
   This is confusing. Which types are natively supported? How do I know if my POJO is supported or not?

##########
File path: modules/api/src/main/java/org/apache/ignite/table/mapper/PojoMapper.java
##########
@@ -0,0 +1,46 @@
+/*
+ * 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.ignite.table.mapper;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Mapper implementation which maps fields of objects of type {@link T} to the columns by their names. Intended to use only with natively
+ * supported types.
+ *
+ * @param <T> Target type.
+ * @see OneColumnMapper
+ */
+public interface PojoMapper<T> extends Mapper<T> {
+    /**
+     * Return a field name for given column name when POJO individual fields are mapped to columns, otherwise fails.

Review comment:
       ```suggestion
        * Returns a field name for a given column name when POJO individual fields are mapped to columns, otherwise fails.
   ```




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [ignite-3] ptupitsyn commented on pull request #455: IGNITE-15784 Mapper support arbitrary serializable user pojo.

Posted by GitBox <gi...@apache.org>.
ptupitsyn commented on pull request #455:
URL: https://github.com/apache/ignite-3/pull/455#issuecomment-981528843


   @AMashenkov In my opinion, we have a few issues here:
   
   * We don't have compute or services for now, so server-side mapping can't be used at all.
   * We don't provide client-side mapping, and it is unclear how to reuse code between client and server, how to ensure the format is interchangeable.
   
   As a result, we may merge some code that does not provide any value to the user and potentially makes our life harder in the future.
   
   My suggestion is to implement mapping for the client side first.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [ignite-3] Berkof commented on a change in pull request #455: IGNITE-15784 Mapper support arbitrary serializable user pojo.

Posted by GitBox <gi...@apache.org>.
Berkof commented on a change in pull request #455:
URL: https://github.com/apache/ignite-3/pull/455#discussion_r753982627



##########
File path: modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/MarshallerUtil.java
##########
@@ -45,32 +46,31 @@
     public static int getValueSize(Object val, NativeType type) throws InvalidTypeException {
         switch (type.spec()) {
             case BYTES:
-                return ((byte[]) val).length;
-            
+                // Return zero for pojo as they are not serialized yet.
+                return (val instanceof byte[]) ? ((byte[]) val).length : 0;

Review comment:
       Is it worth setting some small value to guess object size? With zero we'll underestimate it in 100% cases, but with 100 bytes (some constant) we have a chance to guess.




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [ignite-3] AMashenkov commented on a change in pull request #455: IGNITE-15784 Mapper support arbitrary serializable user pojo.

Posted by GitBox <gi...@apache.org>.
AMashenkov commented on a change in pull request #455:
URL: https://github.com/apache/ignite-3/pull/455#discussion_r760571449



##########
File path: modules/api/src/main/java/org/apache/ignite/table/mapper/DefaultColumnMapper.java
##########
@@ -21,7 +21,7 @@
 import org.jetbrains.annotations.NotNull;
 
 /**
- * Simple mapper implementation that map a column to a class field.
+ * Mapper implementation which maps object fields to the columns by their names.

Review comment:
       For now, we allow only one-to-one mapping. (otherwise 'object <-> row' translation looks ambiguous)
   
   So, seems, we will need both methods to resolve: column->field and field->column.
   As columns must be written to a row in order, we need dummy-mapping for missed columns (to write 'null' or 'column default'). Resolving 'column->field' allow us to write columns in right order and write a row in one iteration.
   
   On the client side, we may not know all the columns and their defaults.
   Resolving 'field->column' allow here to write all fields to a tuple (or whatever client protocol sends to a server in MsgPack format).
   However, I've no idea how to deal with orphan fields here (when no column exists that matches field name).
   * should we pass them to a server and then bypass validation and ignore orphans.
   * disallow orphans and fails with SchemaMismatch exception.
   




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [ignite-3] AMashenkov merged pull request #455: IGNITE-15784 Mapper support arbitrary serializable user pojo.

Posted by GitBox <gi...@apache.org>.
AMashenkov merged pull request #455:
URL: https://github.com/apache/ignite-3/pull/455


   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [ignite-3] AMashenkov commented on a change in pull request #455: IGNITE-15784 Mapper support arbitrary serializable user pojo.

Posted by GitBox <gi...@apache.org>.
AMashenkov commented on a change in pull request #455:
URL: https://github.com/apache/ignite-3/pull/455#discussion_r768469684



##########
File path: modules/api/src/main/java/org/apache/ignite/table/mapper/MapperBuilder.java
##########
@@ -37,93 +50,205 @@
     /** Column-to-field name mapping. */
     private Map<String, String> columnToFields;
 
+    /** Column converters. */
+    private Map<String, TypeConverter<?, ?>> columnConverters = new HashMap<>();
+
+    /** Column name for one-column mapping. */
+    private final String mappedToColumn;
+
+    /** Flag indicates, if all unmapped object fields should be mapped to the columns with same names automatically. */
+    private boolean automapFlag = false;
+
+    /** {@code True} if the {@link #build()} method was called, {@code false} otherwise. */
+    private boolean isStale;
+
     /**
-     * Creates a mapper builder for a type.
+     * Creates a mapper builder for a POJO type.
      *
      * @param targetType Target type.
      */
     MapperBuilder(@NotNull Class<T> targetType) {
-        this.targetType = targetType;
+        this.targetType = Mapper.ensureValidPojo(targetType);
 
+        mappedToColumn = null;
         columnToFields = new HashMap<>(targetType.getDeclaredFields().length);
     }
 
     /**
-     * Add mapping for a field to a column.
+     * Creates a mapper builder for a natively supported type.
+     *
+     * @param targetType   Target type.
+     * @param mappedColumn Column name to map to.
+     */
+    MapperBuilder(@NotNull Class<T> targetType, String mappedColumn) {
+        this.targetType = Mapper.ensureNativelySupported(targetType);
+
+        mappedToColumn = mappedColumn;
+        columnToFields = null;
+    }
+
+    /**
+     * Ensure this instance is valid.
      *
-     * @param fieldName  Field name.
-     * @param columnName Column name.
-     * @return {@code this} for chaining.
-     * @throws IllegalArgumentException if a column was already mapped to some field.
      * @throws IllegalStateException if tries to reuse the builder after a mapping has been built.
      */
-    public MapperBuilder<T> map(@NotNull String fieldName, @NotNull String columnName) {
-        if (columnToFields == null) {
+    private void ensureNotStale() {
+        if (isStale) {
             throw new IllegalStateException("Mapper builder can't be reused.");
         }
+    }
+
+    /**
+     * Ensure field name is valid and field with this name exists.
+     *
+     * @param fieldName Field name.
+     * @return Field name for chaining.
+     * @throws IllegalArgumentException If field is {@code null} or class has no declared field with given name.
+     */
+    private String requireValidField(String fieldName) {
+        try {
+            if (fieldName == null || targetType.getDeclaredField(fieldName) == null) {
+                throw new IllegalArgumentException("Mapping for a column already exists: " + fieldName);
+            }
+        } catch (NoSuchFieldException e) {
+            throw new IllegalArgumentException(
+                    String.format("Field not found for class: field=%s, class=%s", fieldName, targetType.getName()));
+        }
+
+        return fieldName;
+    }
+
+    /**
+     * Maps a field to a column.
+     *
+     * @param fieldName        Field name.
+     * @param columnName       Column name.
+     * @param fieldColumnPairs Vararg that accepts (fieldName, columnName) pairs.
+     * @return {@code this} for chaining.
+     * @throws IllegalArgumentException If a field name has not paired column name in {@code fieldColumnPairs}, or a column was already
+     *                                  mapped to another field.
+     * @throws IllegalStateException    if tries to reuse the builder after a mapping has been built.
+     */
+    public MapperBuilder<T> map(@NotNull String fieldName, @NotNull String columnName, String... fieldColumnPairs) {
+        ensureNotStale();
 
-        if (columnToFields.put(Objects.requireNonNull(columnName), Objects.requireNonNull(fieldName)) != null) {
+        if (columnToFields == null) {
+            throw new IllegalArgumentException("Natively supported types doesn't support field mapping.");
+        } else if (fieldColumnPairs.length % 2 != 0) {
+            throw new IllegalArgumentException("Missed a column name, which the field is mapped to.");
+        } else if (columnToFields.put(Objects.requireNonNull(columnName), requireValidField(fieldName)) != null) {
             throw new IllegalArgumentException("Mapping for a column already exists: " + columnName);
         }
 
+        for (int i = 0; i < fieldColumnPairs.length; i += 2) {
+            if (columnToFields.put(Objects.requireNonNull(fieldColumnPairs[i + 1]), requireValidField(fieldColumnPairs[i])) != null) {
+                throw new IllegalArgumentException("Mapping for a column already exists: " + columnName);

Review comment:
       It may be hard to detect wrong mapping in case of typo or the builder reusing.
   I think, if the one overwrites the previous mapping, it more like to be a mistake.




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [ignite-3] AMashenkov commented on a change in pull request #455: IGNITE-15784 Mapper support arbitrary serializable user pojo.

Posted by GitBox <gi...@apache.org>.
AMashenkov commented on a change in pull request #455:
URL: https://github.com/apache/ignite-3/pull/455#discussion_r755908804



##########
File path: modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/MarshallerUtil.java
##########
@@ -45,32 +46,31 @@
     public static int getValueSize(Object val, NativeType type) throws InvalidTypeException {
         switch (type.spec()) {
             case BYTES:
-                return ((byte[]) val).length;
-            
+                // Return zero for pojo as they are not serialized yet.
+                return (val instanceof byte[]) ? ((byte[]) val).length : 0;

Review comment:
       I'm not sure we can guess anything here, it depends on serialization. Also, it is easy to over-estimate if there are many fields with pojos. Maybe we can disable row size calculation if there is non-null field with user object and use predefined initial buffer size.
   
   As an alternative, we can try to serialize object to a 'blackhole' using configured serializer just to count result byte[] size (without any allocation). This requires some benchmarks. 




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [ignite-3] ptupitsyn commented on a change in pull request #455: IGNITE-15784 Mapper support arbitrary serializable user pojo.

Posted by GitBox <gi...@apache.org>.
ptupitsyn commented on a change in pull request #455:
URL: https://github.com/apache/ignite-3/pull/455#discussion_r758258304



##########
File path: modules/api/src/main/java/org/apache/ignite/table/mapper/DefaultColumnMapper.java
##########
@@ -21,7 +21,7 @@
 import org.jetbrains.annotations.NotNull;
 
 /**
- * Simple mapper implementation that map a column to a class field.
+ * Mapper implementation which maps object fields to the columns by their names.

Review comment:
       Which one of the following mappings do we want to achieve:
   1. Field -> Column
   2. Column -> Field
   3. All of the above?
   
   Javadoc says field -> column, but `mappedField` does column -> field, and `mappedColumn` is not clear at all.
   




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [ignite-3] AMashenkov commented on pull request #455: IGNITE-15784 Mapper support arbitrary serializable user pojo.

Posted by GitBox <gi...@apache.org>.
AMashenkov commented on pull request #455:
URL: https://github.com/apache/ignite-3/pull/455#issuecomment-981514647


   @ptupitsyn ,
   Agree, the client must have same capabilities.
   However, 
   1. I'm not sure if the interceptor interface should be in a public part, or not due to safety reasons.
   2. Java serialization doesn't look suitable for this purpose, because it is non cross-platform way.
   
   As for now, we have no strong position on both statements, what will be sufficient to do for merging the PR?
   Just adding the serializer with the tests to a client module?


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [ignite-3] ptupitsyn commented on a change in pull request #455: IGNITE-15784 Mapper support arbitrary serializable user pojo.

Posted by GitBox <gi...@apache.org>.
ptupitsyn commented on a change in pull request #455:
URL: https://github.com/apache/ignite-3/pull/455#discussion_r768576778



##########
File path: modules/api/src/main/java/org/apache/ignite/table/mapper/MapperBuilder.java
##########
@@ -37,93 +50,205 @@
     /** Column-to-field name mapping. */
     private Map<String, String> columnToFields;
 
+    /** Column converters. */
+    private Map<String, TypeConverter<?, ?>> columnConverters = new HashMap<>();
+
+    /** Column name for one-column mapping. */
+    private final String mappedToColumn;
+
+    /** Flag indicates, if all unmapped object fields should be mapped to the columns with same names automatically. */
+    private boolean automapFlag = false;
+
+    /** {@code True} if the {@link #build()} method was called, {@code false} otherwise. */
+    private boolean isStale;
+
     /**
-     * Creates a mapper builder for a type.
+     * Creates a mapper builder for a POJO type.
      *
      * @param targetType Target type.
      */
     MapperBuilder(@NotNull Class<T> targetType) {
-        this.targetType = targetType;
+        this.targetType = Mapper.ensureValidPojo(targetType);
 
+        mappedToColumn = null;
         columnToFields = new HashMap<>(targetType.getDeclaredFields().length);
     }
 
     /**
-     * Add mapping for a field to a column.
+     * Creates a mapper builder for a natively supported type.
+     *
+     * @param targetType   Target type.
+     * @param mappedColumn Column name to map to.
+     */
+    MapperBuilder(@NotNull Class<T> targetType, String mappedColumn) {
+        this.targetType = Mapper.ensureNativelySupported(targetType);
+
+        mappedToColumn = mappedColumn;
+        columnToFields = null;
+    }
+
+    /**
+     * Ensure this instance is valid.
      *
-     * @param fieldName  Field name.
-     * @param columnName Column name.
-     * @return {@code this} for chaining.
-     * @throws IllegalArgumentException if a column was already mapped to some field.
      * @throws IllegalStateException if tries to reuse the builder after a mapping has been built.
      */
-    public MapperBuilder<T> map(@NotNull String fieldName, @NotNull String columnName) {
-        if (columnToFields == null) {
+    private void ensureNotStale() {
+        if (isStale) {
             throw new IllegalStateException("Mapper builder can't be reused.");
         }
+    }
+
+    /**
+     * Ensure field name is valid and field with this name exists.
+     *
+     * @param fieldName Field name.
+     * @return Field name for chaining.
+     * @throws IllegalArgumentException If field is {@code null} or class has no declared field with given name.
+     */
+    private String requireValidField(String fieldName) {
+        try {
+            if (fieldName == null || targetType.getDeclaredField(fieldName) == null) {
+                throw new IllegalArgumentException("Mapping for a column already exists: " + fieldName);
+            }
+        } catch (NoSuchFieldException e) {
+            throw new IllegalArgumentException(
+                    String.format("Field not found for class: field=%s, class=%s", fieldName, targetType.getName()));
+        }
+
+        return fieldName;
+    }
+
+    /**
+     * Maps a field to a column.
+     *
+     * @param fieldName        Field name.
+     * @param columnName       Column name.
+     * @param fieldColumnPairs Vararg that accepts (fieldName, columnName) pairs.
+     * @return {@code this} for chaining.
+     * @throws IllegalArgumentException If a field name has not paired column name in {@code fieldColumnPairs}, or a column was already
+     *                                  mapped to another field.
+     * @throws IllegalStateException    if tries to reuse the builder after a mapping has been built.
+     */
+    public MapperBuilder<T> map(@NotNull String fieldName, @NotNull String columnName, String... fieldColumnPairs) {
+        ensureNotStale();
 
-        if (columnToFields.put(Objects.requireNonNull(columnName), Objects.requireNonNull(fieldName)) != null) {
+        if (columnToFields == null) {
+            throw new IllegalArgumentException("Natively supported types doesn't support field mapping.");
+        } else if (fieldColumnPairs.length % 2 != 0) {
+            throw new IllegalArgumentException("Missed a column name, which the field is mapped to.");
+        } else if (columnToFields.put(Objects.requireNonNull(columnName), requireValidField(fieldName)) != null) {
             throw new IllegalArgumentException("Mapping for a column already exists: " + columnName);
         }
 
+        for (int i = 0; i < fieldColumnPairs.length; i += 2) {
+            if (columnToFields.put(Objects.requireNonNull(fieldColumnPairs[i + 1]), requireValidField(fieldColumnPairs[i])) != null) {
+                throw new IllegalArgumentException("Mapping for a column already exists: " + columnName);

Review comment:
       Ok.




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [ignite-3] korlov42 commented on a change in pull request #455: IGNITE-15784 Mapper support arbitrary serializable user pojo.

Posted by GitBox <gi...@apache.org>.
korlov42 commented on a change in pull request #455:
URL: https://github.com/apache/ignite-3/pull/455#discussion_r756884496



##########
File path: modules/schema/src/test/java/org/apache/ignite/internal/schema/marshaller/MapperTest.java
##########
@@ -27,49 +32,95 @@
  * Columns mappers test.
  */
 public class MapperTest {
-    
+
     @Test
-    public void misleadingMapping() {
+    public void misleadingMapperUsage() {
         // Empty mapping.
-        assertThrows(IllegalStateException.class, () -> Mapper.builderFor(TestObject.class).build());
-        
+        assertThrows(IllegalStateException.class, () -> Mapper.buildFrom(TestObject.class).build());
+
         // Many fields to one column.
-        assertThrows(IllegalArgumentException.class, () -> Mapper.builderFor(TestObject.class)
+        assertThrows(IllegalArgumentException.class, () -> Mapper.buildFrom(TestObject.class)
                 .map("id", "key")
                 .map("longCol", "key"));
-        
+
         // One field to many columns.
-        assertThrows(IllegalStateException.class, () -> Mapper.builderFor(TestObject.class)
+        assertThrows(IllegalStateException.class, () -> Mapper.buildFrom(TestObject.class)
                 .map("id", "key")
                 .map("id", "val1")
                 .map("stringCol", "val2")
                 .build());
-        
+
         // Mapper builder reuse fails.
         assertThrows(IllegalStateException.class, () -> {
-            MapperBuilder<TestObject> builder = Mapper.builderFor(TestObject.class)
+            MapperBuilder<TestObject> builder = Mapper.buildFrom(TestObject.class)
                     .map("id", "key");
-            
+
             builder.build();
-            
+
             builder.map("stringCol", "val2");
         });
     }
-    
+
+    @Test
+    public void supportedClassKinds() {
+        class LocalClass {
+            long id;
+        }
+
+        Function anonymous = (i) -> i;
+
+        Mapper.buildFrom(TestOuterObject.class);
+        Mapper.buildFrom(NestedObject.class);
+
+        assertThrows(IllegalArgumentException.class, () -> Mapper.buildFrom(Long.class));
+        assertThrows(IllegalArgumentException.class, () -> Mapper.buildFrom(TestOuterObject.InnerObject.class));
+        assertThrows(IllegalArgumentException.class, () -> Mapper.buildFrom(LocalClass.class));
+        assertThrows(IllegalArgumentException.class, () -> Mapper.buildFrom(anonymous.getClass()));

Review comment:
       shall we verify arrays and collections as well?




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [ignite-3] AMashenkov commented on a change in pull request #455: IGNITE-15784 Mapper support arbitrary serializable user pojo.

Posted by GitBox <gi...@apache.org>.
AMashenkov commented on a change in pull request #455:
URL: https://github.com/apache/ignite-3/pull/455#discussion_r760571449



##########
File path: modules/api/src/main/java/org/apache/ignite/table/mapper/DefaultColumnMapper.java
##########
@@ -21,7 +21,7 @@
 import org.jetbrains.annotations.NotNull;
 
 /**
- * Simple mapper implementation that map a column to a class field.
+ * Mapper implementation which maps object fields to the columns by their names.

Review comment:
       For now, we allow only one-to-one mapping. (otherwise 'object <-> row' translation looks ambiguous)
   
   So, seems, we will need both methods to resolve: column->field and field->column.
   As columns must be written to a row in order, we need dummy-mapping for missed columns (to write 'null' or 'column default'). Resolving 'column->field' allow us to write columns in right order and write a row in one iteration.
   
   On the client side, we may not know all the columns and their defaults. Do we know and I'm wrong?
   Resolving 'field->column' allow here to write all fields to a tuple (or whatever client protocol sends to a server in MsgPack format).
   However, I've no idea how to deal with orphan fields, when no column exists that matches field name.
   * should we pass them to a server and then bypass validation and ignore orphans.
   * disallow orphans and fails with SchemaMismatch exception.
   




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [ignite-3] ptupitsyn commented on a change in pull request #455: IGNITE-15784 Mapper support arbitrary serializable user pojo.

Posted by GitBox <gi...@apache.org>.
ptupitsyn commented on a change in pull request #455:
URL: https://github.com/apache/ignite-3/pull/455#discussion_r760940470



##########
File path: modules/api/src/main/java/org/apache/ignite/table/mapper/DefaultColumnMapper.java
##########
@@ -21,7 +21,7 @@
 import org.jetbrains.annotations.NotNull;
 
 /**
- * Simple mapper implementation that map a column to a class field.
+ * Mapper implementation which maps object fields to the columns by their names.

Review comment:
       I think we should have two-way mapping, column -> field and field -> column.
   
   We know the schema on the client side (column names and their order). We don't know defaults for now, but I don't think they are required.
   
   It should be easy to write the row in correct order from an object if we have `column -> field` mapping:
   
   ```
   for (var col : schema.columns()) {
       var fieldName = mapper.fieldForColumn(col.name());
       if (fieldName == null)
           packNil();
       else
           packValue(...);
   }
   ```




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [ignite-3] AMashenkov edited a comment on pull request #455: IGNITE-15784 Mapper support arbitrary serializable user pojo.

Posted by GitBox <gi...@apache.org>.
AMashenkov edited a comment on pull request #455:
URL: https://github.com/apache/ignite-3/pull/455#issuecomment-981514647


   @ptupitsyn ,
   Agree, the client must have same capabilities.
   However, 
   1. I'm not sure if the interceptor interface should be in a public part, or not, due to safety reasons.
   2. Java serialization doesn't look suitable for this purpose, because it is non cross-platform way.
   
   As for now, we have no strong position on both statements, what will be sufficient to do for merging the PR?
   Just adding the serializer with the tests to a client module?


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@ignite.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org