You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by am...@apache.org on 2021/11/12 14:01:39 UTC

[ignite-3] branch main updated: IGNITE-14484 Implement RecordView API. (#435)

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

amashenkov pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git


The following commit(s) were added to refs/heads/main by this push:
     new af06c65  IGNITE-14484 Implement RecordView API. (#435)
af06c65 is described below

commit af06c65ae41ce7295a03d43d1e08f26dfca092c2
Author: Andrew V. Mashenkov <AM...@users.noreply.github.com>
AuthorDate: Fri Nov 12 17:01:00 2021 +0300

    IGNITE-14484 Implement RecordView API. (#435)
---
 .../apache/ignite/table/mapper/MapperBuilder.java  |  28 +-
 .../internal/schema/marshaller/KvMarshaller.java   |   5 +
 .../schema/marshaller/MarshallerFactory.java       |  22 +-
 .../schema/marshaller/RecordMarshaller.java}       |  40 +-
 .../schema/marshaller/SerializerFactory.java       |   1 +
 .../marshaller/reflection/JavaSerializer.java      |   1 +
 .../reflection/JavaSerializerFactory.java          |   1 +
 .../marshaller/reflection/KvMarshallerImpl.java    |  12 +-
 .../schema/marshaller/reflection/Marshaller.java   |  15 +-
 ...rshallerImpl.java => RecordMarshallerImpl.java} |  87 ++--
 .../reflection/ReflectionMarshallerFactory.java    |   6 +
 .../schema/marshaller/JavaSerializerTest.java      |  62 ++-
 .../schema/marshaller/KvMarshallerTest.java        | 139 ++++--
 .../internal/schema/marshaller/MapperTest.java     |  75 +++
 ...rshallerTest.java => RecordMarshallerTest.java} | 539 +++++++++------------
 .../TestObjectWithNoDefaultConstructor.java        |  15 +-
 .../TestObjectWithPrivateConstructor.java          |   9 +-
 .../ignite/internal/table/KeyValueViewImpl.java    |  12 +-
 .../ignite/internal/table/RecordViewImpl.java      | 237 ++++++---
 .../internal/table/KeyValueViewOperationsTest.java |   6 +-
 .../internal/table/RecordViewOperationsTest.java   | 340 +++++++++++++
 21 files changed, 1111 insertions(+), 541 deletions(-)

diff --git a/modules/api/src/main/java/org/apache/ignite/table/mapper/MapperBuilder.java b/modules/api/src/main/java/org/apache/ignite/table/mapper/MapperBuilder.java
index ee290f2..e1357e2 100644
--- a/modules/api/src/main/java/org/apache/ignite/table/mapper/MapperBuilder.java
+++ b/modules/api/src/main/java/org/apache/ignite/table/mapper/MapperBuilder.java
@@ -18,6 +18,7 @@
 package org.apache.ignite.table.mapper;
 
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Map;
 import java.util.Objects;
 import java.util.function.Function;
@@ -34,7 +35,7 @@ public final class MapperBuilder<T> {
     private Class<T> targetType;
     
     /** Column-to-field name mapping. */
-    private Map<String, String> mapping;
+    private Map<String, String> columnToFields;
     
     /**
      * Creates a mapper builder for a type.
@@ -44,7 +45,7 @@ public final class MapperBuilder<T> {
     MapperBuilder(@NotNull Class<T> targetType) {
         this.targetType = targetType;
         
-        mapping = new HashMap<>(targetType.getDeclaredFields().length);
+        columnToFields = new HashMap<>(targetType.getDeclaredFields().length);
     }
     
     /**
@@ -53,13 +54,15 @@ public final class MapperBuilder<T> {
      * @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 (mapping == null) {
+        if (columnToFields == null) {
             throw new IllegalStateException("Mapper builder can't be reused.");
         }
         
-        if (mapping.put(Objects.requireNonNull(columnName), Objects.requireNonNull(fieldName)) != null) {
+        if (columnToFields.put(Objects.requireNonNull(columnName), Objects.requireNonNull(fieldName)) != null) {
             throw new IllegalArgumentException("Mapping for a column already exists: " + columnName);
         }
         
@@ -102,11 +105,24 @@ public final class MapperBuilder<T> {
      * Builds mapper.
      *
      * @return Mapper.
+     * @throws IllegalStateException if nothing were mapped or more than one column were mapped to the same field.
      */
     public Mapper<T> build() {
-        Map<String, String> mapping = this.mapping;
+        if (columnToFields.isEmpty()) {
+            throw new IllegalStateException("Empty mapping isn't allowed.");
+        }
+        
+        Map<String, String> mapping = this.columnToFields;
+        
+        this.columnToFields = null;
         
-        this.mapping = null;
+        HashSet<String> fields = new HashSet<>(mapping.size());
+        
+        for (String f : mapping.values()) {
+            if (!fields.add(f)) {
+                throw new IllegalStateException("More than one column is mapped to the field: field=" + f);
+            }
+        }
         
         return new DefaultColumnMapper<>(targetType, mapping);
     }
diff --git a/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/KvMarshaller.java b/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/KvMarshaller.java
index c772894..3dc6c81 100644
--- a/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/KvMarshaller.java
+++ b/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/KvMarshaller.java
@@ -31,6 +31,11 @@ import org.jetbrains.annotations.Nullable;
  */
 public interface KvMarshaller<K, V> {
     /**
+     * Returns marshaller schema version.
+     */
+    int schemaVersion();
+    
+    /**
      * Marshal given key and value objects to a table row.
      *
      * @param key Key object to marshal.
diff --git a/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/MarshallerFactory.java b/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/MarshallerFactory.java
index b2ceeac..3c8858e 100644
--- a/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/MarshallerFactory.java
+++ b/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/MarshallerFactory.java
@@ -23,7 +23,6 @@ import org.apache.ignite.table.mapper.Mapper;
 /**
  * Marshaller factory interface.
  */
-@FunctionalInterface
 public interface MarshallerFactory {
     /**
      * Creates key-value marshaller using provided mappers.
@@ -49,4 +48,25 @@ public interface MarshallerFactory {
             Class<V> valueClass) {
         return create(schema, Mapper.identity(keyClass), Mapper.identity(valueClass));
     }
+    
+    /**
+     * Creates record marshaller using provided mapper.
+     *
+     * @param schema Schema descriptor.
+     * @param mapper Record mapper.
+     * @return Record marshaller.
+     */
+    <R> RecordMarshaller<R> create(SchemaDescriptor schema, Mapper<R> mapper);
+    
+    /**
+     * Shortcut method creates record marshaller for classes using identity mappers.
+     *
+     * @param schema      Schema descriptor.
+     * @param recordClass Record type.
+     * @return Record marshaller.
+     * @see Mapper#identity(Class)
+     */
+    default <R> RecordMarshaller<R> create(SchemaDescriptor schema, Class<R> recordClass) {
+        return create(schema, Mapper.identity(recordClass));
+    }
 }
diff --git a/modules/table/src/main/java/org/apache/ignite/internal/schema/marshaller/RecordSerializer.java b/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/RecordMarshaller.java
similarity index 52%
rename from modules/table/src/main/java/org/apache/ignite/internal/schema/marshaller/RecordSerializer.java
rename to modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/RecordMarshaller.java
index 5792f10..a82f97b 100644
--- a/modules/table/src/main/java/org/apache/ignite/internal/schema/marshaller/RecordSerializer.java
+++ b/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/RecordMarshaller.java
@@ -17,35 +17,45 @@
 
 package org.apache.ignite.internal.schema.marshaller;
 
+import org.apache.ignite.internal.schema.BinaryRow;
 import org.apache.ignite.internal.schema.row.Row;
 import org.jetbrains.annotations.NotNull;
 
 /**
- * Record serializer interface.
+ * Record marshaller interface provides method to marshal/unmarshal record objects to/from a row.
+ *
+ * @param <R> Record type.
  */
-public interface RecordSerializer<R> {
+public interface RecordMarshaller<R> {
+    /**
+     * Returns marshaller schema version.
+     */
+    int schemaVersion();
+    
     /**
-     * Serializes the record.
+     * Marshals given record to a row.
      *
-     * @param red Record to serialize.
+     * @param rec Record to marshal.
      * @return Table row with columns set from given object.
+     * @throws MarshallerException If failed to marshal record.
      */
-    Row serialize(@NotNull R red);
-
+    BinaryRow marshal(@NotNull R rec) throws MarshallerException;
+    
     /**
-     * Deserializes the record.
+     * Marshals key part of given record to a row.
      *
-     * @param row Table row.
-     * @return Deserialized record object.
+     * @param keyRec Record to marshal.
+     * @return Table row with key columns set from given object.
+     * @throws MarshallerException If failed to marshal record.
      */
-    R deserialize(@NotNull Row row);
-
+    BinaryRow marshalKey(@NotNull R keyRec) throws MarshallerException;
+    
     /**
-     * Deserializes row and fills given record object fields.
+     * Unmarshal given row to a record object.
      *
      * @param row Table row.
-     * @param rec Record object to fill.
-     * @return Given record with filled fields from the given row.
+     * @return Record object.
+     * @throws MarshallerException If failed to unmarshal row.
      */
-    R deserialize(@NotNull Row row, @NotNull R rec);
+    R unmarshal(@NotNull Row row) throws MarshallerException;
 }
diff --git a/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/SerializerFactory.java b/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/SerializerFactory.java
index 07fe59e..3d90543 100644
--- a/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/SerializerFactory.java
+++ b/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/SerializerFactory.java
@@ -26,6 +26,7 @@ import org.apache.ignite.internal.schema.marshaller.reflection.JavaSerializerFac
  *
  * @deprecated Replaced with {@link MarshallerFactory}
  */
+//TODO: IGNITE-15888 drop
 @FunctionalInterface
 @Deprecated(forRemoval = true)
 public interface SerializerFactory {
diff --git a/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/reflection/JavaSerializer.java b/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/reflection/JavaSerializer.java
index 1d01416..66e2d12 100644
--- a/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/reflection/JavaSerializer.java
+++ b/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/reflection/JavaSerializer.java
@@ -34,6 +34,7 @@ import org.jetbrains.annotations.Nullable;
 /**
  * Reflection based (de)serializer.
  */
+//TODO: IGNITE-15907 drop
 @Deprecated(forRemoval = true)
 public class JavaSerializer extends AbstractSerializer {
     /** Key class. */
diff --git a/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/reflection/JavaSerializerFactory.java b/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/reflection/JavaSerializerFactory.java
index c77f6d7..ef32a56 100644
--- a/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/reflection/JavaSerializerFactory.java
+++ b/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/reflection/JavaSerializerFactory.java
@@ -24,6 +24,7 @@ import org.apache.ignite.internal.schema.marshaller.SerializerFactory;
 /**
  * Factory for reflection-based serializer.
  */
+//TODO: IGNITE-15907 drop
 @Deprecated(forRemoval = true)
 public class JavaSerializerFactory implements SerializerFactory {
     /** {@inheritDoc} */
diff --git a/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/reflection/KvMarshallerImpl.java b/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/reflection/KvMarshallerImpl.java
index e234a3b..54d284a 100644
--- a/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/reflection/KvMarshallerImpl.java
+++ b/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/reflection/KvMarshallerImpl.java
@@ -23,6 +23,7 @@ import java.util.Objects;
 import org.apache.ignite.internal.schema.BinaryRow;
 import org.apache.ignite.internal.schema.ByteBufferRow;
 import org.apache.ignite.internal.schema.Columns;
+import org.apache.ignite.internal.schema.NativeType;
 import org.apache.ignite.internal.schema.SchemaDescriptor;
 import org.apache.ignite.internal.schema.marshaller.KvMarshaller;
 import org.apache.ignite.internal.schema.marshaller.MarshallerException;
@@ -63,10 +64,12 @@ public class KvMarshallerImpl<K, V> implements KvMarshaller<K, V> {
         keyClass = keyMapper.targetType();
         valClass = valueMapper.targetType();
         
-        keyMarsh = Marshaller.createMarshaller(schema.keyColumns(), keyMapper);
-        valMarsh = Marshaller.createMarshaller(schema.valueColumns(), valueMapper);
+        keyMarsh = Marshaller.createMarshaller(schema.keyColumns().columns(), keyMapper);
+        valMarsh = Marshaller.createMarshaller(schema.valueColumns().columns(), valueMapper);
     }
     
+    /** {@inheritDoc} */
+    @Override
     public int schemaVersion() {
         return schema.version();
     }
@@ -147,12 +150,13 @@ public class KvMarshallerImpl<K, V> implements KvMarshaller<K, V> {
         
         for (int i = cols.firstVarlengthColumn(); i < cols.length(); i++) {
             final Object val = marsh.value(obj, i);
+            final NativeType colType = cols.column(i).type();
             
-            if (val == null || cols.column(i).type().spec().fixedLength()) {
+            if (val == null || colType.spec().fixedLength()) {
                 continue;
             }
             
-            size += getValueSize(val, cols.column(i).type());
+            size += getValueSize(val, colType);
             cnt++;
         }
         
diff --git a/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/reflection/Marshaller.java b/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/reflection/Marshaller.java
index c933354..cbe2137 100644
--- a/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/reflection/Marshaller.java
+++ b/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/reflection/Marshaller.java
@@ -41,24 +41,24 @@ public abstract class Marshaller {
      * @param mapper Mapper.
      * @return Marshaller.
      */
-    public static <T> Marshaller createMarshaller(Columns cols, Mapper<T> mapper) {
+    public static <T> Marshaller createMarshaller(Column[] cols, Mapper<T> mapper) {
         final BinaryMode mode = MarshallerUtil.mode(mapper.targetType());
         
         if (mode != null) {
-            final Column col = cols.column(0);
+            final Column col = cols[0];
             
-            assert cols.length() == 1;
+            assert cols.length == 1;
             assert mode.typeSpec() == col.type().spec() : "Target type is not compatible.";
             assert !mapper.targetType().isPrimitive() : "Non-nullable types are not allowed.";
             
             return new SimpleMarshaller(FieldAccessor.createIdentityAccessor(col, col.schemaIndex(), mode));
         }
         
-        FieldAccessor[] fieldAccessors = new FieldAccessor[cols.length()];
+        FieldAccessor[] fieldAccessors = new FieldAccessor[cols.length];
         
         // Build handlers.
-        for (int i = 0; i < cols.length(); i++) {
-            final Column col = cols.column(i);
+        for (int i = 0; i < cols.length; i++) {
+            final Column col = cols[i];
             
             String fieldName = mapper.columnToField(col.name());
             
@@ -77,7 +77,8 @@ public abstract class Marshaller {
      * @param cls  Type.
      * @return Marshaller.
      */
-    @Deprecated // TODO drop method.
+    //TODO: IGNITE-15907 drop
+    @Deprecated
     public static Marshaller createMarshaller(Columns cols, Class<? extends Object> cls) {
         final BinaryMode mode = MarshallerUtil.mode(cls);
         
diff --git a/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/reflection/KvMarshallerImpl.java b/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/reflection/RecordMarshallerImpl.java
similarity index 69%
copy from modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/reflection/KvMarshallerImpl.java
copy to modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/reflection/RecordMarshallerImpl.java
index e234a3b..ae2aef9 100644
--- a/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/reflection/KvMarshallerImpl.java
+++ b/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/reflection/RecordMarshallerImpl.java
@@ -22,96 +22,90 @@ import static org.apache.ignite.internal.schema.marshaller.MarshallerUtil.getVal
 import java.util.Objects;
 import org.apache.ignite.internal.schema.BinaryRow;
 import org.apache.ignite.internal.schema.ByteBufferRow;
+import org.apache.ignite.internal.schema.Column;
 import org.apache.ignite.internal.schema.Columns;
 import org.apache.ignite.internal.schema.SchemaDescriptor;
-import org.apache.ignite.internal.schema.marshaller.KvMarshaller;
 import org.apache.ignite.internal.schema.marshaller.MarshallerException;
+import org.apache.ignite.internal.schema.marshaller.RecordMarshaller;
 import org.apache.ignite.internal.schema.row.Row;
 import org.apache.ignite.internal.schema.row.RowAssembler;
+import org.apache.ignite.internal.util.ArrayUtils;
 import org.apache.ignite.table.mapper.Mapper;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /**
- * Key-value marshaller for given schema and mappers.
+ * Record marshaller for given schema and mappers.
  *
- * @param <K> Key type.
- * @param <V> Value type.
+ * @param <R> Record type.
  */
-public class KvMarshallerImpl<K, V> implements KvMarshaller<K, V> {
+public class RecordMarshallerImpl<R> implements RecordMarshaller<R> {
     /** Schema. */
     private final SchemaDescriptor schema;
     
     /** Key marshaller. */
     private final Marshaller keyMarsh;
     
-    /** Value marshaller. */
-    private final Marshaller valMarsh;
+    /** Record marshaller. */
+    private final Marshaller recMarsh;
     
-    /** Key type. */
-    private final Class<K> keyClass;
-    
-    /** Value type. */
-    private final Class<V> valClass;
+    /** Record type. */
+    private final Class<R> recClass;
     
     /**
      * Creates KV marshaller.
      */
-    public KvMarshallerImpl(SchemaDescriptor schema, Mapper<K> keyMapper, Mapper<V> valueMapper) {
+    public RecordMarshallerImpl(SchemaDescriptor schema, Mapper<R> mapper) {
         this.schema = schema;
         
-        keyClass = keyMapper.targetType();
-        valClass = valueMapper.targetType();
+        recClass = mapper.targetType();
+        
+        keyMarsh = Marshaller.createMarshaller(schema.keyColumns().columns(), mapper);
         
-        keyMarsh = Marshaller.createMarshaller(schema.keyColumns(), keyMapper);
-        valMarsh = Marshaller.createMarshaller(schema.valueColumns(), valueMapper);
+        recMarsh = Marshaller.createMarshaller(
+                ArrayUtils.concat(schema.keyColumns().columns(), schema.valueColumns().columns()),
+                mapper
+        );
     }
     
+    /** {@inheritDoc} */
+    @Override
     public int schemaVersion() {
         return schema.version();
     }
     
     /** {@inheritDoc} */
     @Override
-    public BinaryRow marshal(@NotNull K key, V val) throws MarshallerException {
-        assert keyClass.isInstance(key);
-        assert val == null || valClass.isInstance(val);
-        
-        final RowAssembler asm = createAssembler(Objects.requireNonNull(key), val);
+    public BinaryRow marshal(@NotNull R rec) throws MarshallerException {
+        assert recClass.isInstance(rec);
         
-        keyMarsh.writeObject(key, asm);
+        final RowAssembler asm = createAssembler(Objects.requireNonNull(rec), rec);
         
-        if (val != null) {
-            valMarsh.writeObject(val, asm);
-        }
+        recMarsh.writeObject(rec, asm);
         
         return new ByteBufferRow(asm.toBytes());
     }
     
     /** {@inheritDoc} */
-    @NotNull
     @Override
-    public K unmarshalKey(@NotNull Row row) throws MarshallerException {
-        final Object o = keyMarsh.readObject(row);
+    public BinaryRow marshalKey(@NotNull R rec) throws MarshallerException {
+        assert recClass.isInstance(rec);
+        
+        final RowAssembler asm = createAssembler(Objects.requireNonNull(rec), null);
         
-        assert keyClass.isInstance(o);
+        keyMarsh.writeObject(rec, asm);
         
-        return (K) o;
+        return new ByteBufferRow(asm.toBytes());
     }
     
     /** {@inheritDoc} */
-    @Nullable
+    @NotNull
     @Override
-    public V unmarshalValue(@NotNull Row row) throws MarshallerException {
-        if (!row.hasValue()) {
-            return null;
-        }
-        
-        final Object o = valMarsh.readObject(row);
+    public R unmarshal(@NotNull Row row) throws MarshallerException {
+        final Object o = recMarsh.readObject(row);
         
-        assert o == null || valClass.isInstance(o);
+        assert recClass.isInstance(o);
         
-        return (V) o;
+        return (R) o;
     }
     
     /**
@@ -122,8 +116,8 @@ public class KvMarshallerImpl<K, V> implements KvMarshaller<K, V> {
      * @return Row assembler.
      */
     private RowAssembler createAssembler(Object key, Object val) {
-        ObjectStatistic keyStat = collectObjectStats(schema.keyColumns(), keyMarsh, key);
-        ObjectStatistic valStat = collectObjectStats(schema.valueColumns(), valMarsh, val);
+        ObjectStatistic keyStat = collectObjectStats(schema.keyColumns(), recMarsh, key);
+        ObjectStatistic valStat = collectObjectStats(schema.valueColumns(), recMarsh, val);
         
         return new RowAssembler(schema, keyStat.nonNullColsSize, keyStat.nonNullCols,
                 valStat.nonNullColsSize, valStat.nonNullCols);
@@ -146,13 +140,14 @@ public class KvMarshallerImpl<K, V> implements KvMarshaller<K, V> {
         int size = 0;
         
         for (int i = cols.firstVarlengthColumn(); i < cols.length(); i++) {
-            final Object val = marsh.value(obj, i);
+            final Column column = cols.column(i);
+            final Object val = marsh.value(obj, column.schemaIndex());
             
-            if (val == null || cols.column(i).type().spec().fixedLength()) {
+            if (val == null || column.type().spec().fixedLength()) {
                 continue;
             }
             
-            size += getValueSize(val, cols.column(i).type());
+            size += getValueSize(val, column.type());
             cnt++;
         }
         
diff --git a/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/reflection/ReflectionMarshallerFactory.java b/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/reflection/ReflectionMarshallerFactory.java
index b3d9a89..654b735 100644
--- a/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/reflection/ReflectionMarshallerFactory.java
+++ b/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/reflection/ReflectionMarshallerFactory.java
@@ -20,6 +20,7 @@ package org.apache.ignite.internal.schema.marshaller.reflection;
 import org.apache.ignite.internal.schema.SchemaDescriptor;
 import org.apache.ignite.internal.schema.marshaller.KvMarshaller;
 import org.apache.ignite.internal.schema.marshaller.MarshallerFactory;
+import org.apache.ignite.internal.schema.marshaller.RecordMarshaller;
 import org.apache.ignite.table.mapper.Mapper;
 
 /**
@@ -30,4 +31,9 @@ public class ReflectionMarshallerFactory implements MarshallerFactory {
     @Override public <K, V> KvMarshaller<K, V> create(SchemaDescriptor schema, Mapper<K> keyMapper, Mapper<V> valueMapper) {
         return new KvMarshallerImpl<>(schema, keyMapper, valueMapper);
     }
+    
+    /** {@inheritDoc} */
+    @Override public <R> RecordMarshaller<R> create(SchemaDescriptor schema, Mapper<R> mapper) {
+        return new RecordMarshallerImpl<>(schema, mapper);
+    }
 }
diff --git a/modules/schema/src/test/java/org/apache/ignite/internal/schema/marshaller/JavaSerializerTest.java b/modules/schema/src/test/java/org/apache/ignite/internal/schema/marshaller/JavaSerializerTest.java
index dc34a06..453d9ad 100644
--- a/modules/schema/src/test/java/org/apache/ignite/internal/schema/marshaller/JavaSerializerTest.java
+++ b/modules/schema/src/test/java/org/apache/ignite/internal/schema/marshaller/JavaSerializerTest.java
@@ -48,6 +48,7 @@ import com.facebook.presto.bytecode.Variable;
 import com.facebook.presto.bytecode.expression.BytecodeExpressions;
 import java.util.EnumSet;
 import java.util.List;
+import java.util.Objects;
 import java.util.Random;
 import java.util.stream.Stream;
 import javax.annotation.processing.Generated;
@@ -75,6 +76,8 @@ import org.junit.jupiter.params.provider.MethodSource;
 /**
  * Serializer test.
  */
+//TODO: IGNITE-15888 drop
+@Deprecated(forRemoval = true)
 public class JavaSerializerTest {
     /**
      * Get list of serializers for test.
@@ -176,7 +179,6 @@ public class JavaSerializerTest {
 
         BinaryRow row = serializer.serialize(key, val);
 
-        // Try different order.
         Object restoredVal = serializer.deserializeValue(new Row(schema, row));
         Object restoredKey = serializer.deserializeKey(new Row(schema, row));
 
@@ -246,6 +248,7 @@ public class JavaSerializerTest {
     public void classWithPrivateConstructor(SerializerFactory factory) throws MarshallerException {
         Column[] cols = new Column[]{
                 new Column("primLongCol", INT64, false),
+                new Column("primIntCol", INT32, false)
         };
 
         SchemaDescriptor schema = new SchemaDescriptor(1, cols, cols);
@@ -276,12 +279,14 @@ public class JavaSerializerTest {
     public void classWithNoDefaultConstructor(SerializerFactory factory) {
         Column[] cols = new Column[]{
                 new Column("primLongCol", INT64, false),
+                new Column("primIntCol", INT32, false)
         };
 
         SchemaDescriptor schema = new SchemaDescriptor(1, cols, cols);
 
         final Object key = TestObjectWithNoDefaultConstructor.randomObject(rnd);
         final Object val = TestObjectWithNoDefaultConstructor.randomObject(rnd);
+
         assertThrows(IgniteInternalException.class, () -> factory.create(schema, key.getClass(), val.getClass()));
     }
 
@@ -298,10 +303,10 @@ public class JavaSerializerTest {
 
         SchemaDescriptor schema = new SchemaDescriptor(1, cols, cols);
 
-        final Object key = TestObjectWithPrivateConstructor.randomObject(rnd);
-        final Object val = TestObjectWithPrivateConstructor.randomObject(rnd);
+        final Object key = PrivateTestObject.randomObject(rnd);
+        final Object val = PrivateTestObject.randomObject(rnd);
 
-        final ObjectFactory<?> objFactory = new ObjectFactory<>(TestObjectWithPrivateConstructor.class);
+        final ObjectFactory<?> objFactory = new ObjectFactory<>(PrivateTestObject.class);
         final Serializer serializer = factory.create(schema, key.getClass(), val.getClass());
 
         BinaryRow row = serializer.serialize(key, objFactory.create());
@@ -453,4 +458,53 @@ public class JavaSerializerTest {
                 .dumpRawBytecode(true)
                 .defineClass(classDef, Object.class);
     }
+
+    /**
+     * Test object without default constructor.
+     */
+    @SuppressWarnings("InstanceVariableMayNotBeInitialized")
+    private static class PrivateTestObject {
+        /**
+         * Get random TestObject.
+         */
+        static PrivateTestObject randomObject(Random rnd) {
+            return new PrivateTestObject(rnd.nextInt());
+        }
+
+        /** Value. */
+        private long primLongCol;
+
+        /** Constructor. */
+        PrivateTestObject() {
+        }
+
+        /**
+         * Private constructor.
+         */
+        PrivateTestObject(long val) {
+            primLongCol = val;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+
+            if (o == null || getClass() != o.getClass()) {
+                return false;
+            }
+
+            PrivateTestObject object = (PrivateTestObject) o;
+
+            return primLongCol == object.primLongCol;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public int hashCode() {
+            return Objects.hash(primLongCol);
+        }
+    }
 }
diff --git a/modules/schema/src/test/java/org/apache/ignite/internal/schema/marshaller/KvMarshallerTest.java b/modules/schema/src/test/java/org/apache/ignite/internal/schema/marshaller/KvMarshallerTest.java
index 87b1c96..0a024ad 100644
--- a/modules/schema/src/test/java/org/apache/ignite/internal/schema/marshaller/KvMarshallerTest.java
+++ b/modules/schema/src/test/java/org/apache/ignite/internal/schema/marshaller/KvMarshallerTest.java
@@ -148,7 +148,6 @@ public class KvMarshallerTest {
         
         BinaryRow row = marshaller.marshal(key, val);
         
-        // Try different order.
         TestObjectWithAllTypes restoredVal = marshaller.unmarshalValue(new Row(schema, row));
         TestObjectWithAllTypes restoredKey = marshaller.unmarshalKey(new Row(schema, row));
         
@@ -174,7 +173,6 @@ public class KvMarshallerTest {
         
         BinaryRow row = marshaller.marshal(key, val);
         
-        // Try different order.
         Object restoredVal = marshaller.unmarshalValue(new Row(schema, row));
         Object restoredKey = marshaller.unmarshalKey(new Row(schema, row));
         
@@ -204,7 +202,6 @@ public class KvMarshallerTest {
         
         BinaryRow row = marshaller.marshal(key, val);
         
-        // Try different order.
         TestObjectWithAllTypes restoredVal = marshaller.unmarshalValue(new Row(schema, row));
         TestObjectWithAllTypes restoredKey = marshaller.unmarshalKey(new Row(schema, row));
         
@@ -224,6 +221,7 @@ public class KvMarshallerTest {
         assertEquals(expectedKey, restoredKey);
         assertEquals(expectedVal, restoredVal);
         
+        // Check non-mapped fields has default values.
         assertNull(restoredKey.getUuidCol());
         assertNull(restoredVal.getUuidCol());
         assertEquals(0, restoredKey.getPrimitiveIntCol());
@@ -257,7 +255,6 @@ public class KvMarshallerTest {
         
         BinaryRow row = marshaller.marshal(key, val);
         
-        // Try different order.
         Object restoredVal = marshaller.unmarshalValue(new Row(schema, row));
         Object restoredKey = marshaller.unmarshalKey(new Row(schema, row));
         
@@ -321,6 +318,7 @@ public class KvMarshallerTest {
     public void classWithPrivateConstructor(MarshallerFactory factory) throws MarshallerException {
         Column[] cols = new Column[]{
                 new Column("primLongCol", INT64, false),
+                new Column("primIntCol", INT32, false),
         };
         
         SchemaDescriptor schema = new SchemaDescriptor(1, cols, cols);
@@ -367,12 +365,12 @@ public class KvMarshallerTest {
         
         SchemaDescriptor schema = new SchemaDescriptor(1, cols, cols);
         
-        final ObjectFactory<TestObjectWithPrivateConstructor> objFactory = new ObjectFactory<>(TestObjectWithPrivateConstructor.class);
-        final KvMarshaller<TestObjectWithPrivateConstructor, TestObjectWithPrivateConstructor> marshaller =
-                factory.create(schema, TestObjectWithPrivateConstructor.class, TestObjectWithPrivateConstructor.class);
+        final ObjectFactory<PrivateTestObject> objFactory = new ObjectFactory<>(PrivateTestObject.class);
+        final KvMarshaller<PrivateTestObject, PrivateTestObject> marshaller =
+                factory.create(schema, PrivateTestObject.class, PrivateTestObject.class);
         
-        final TestObjectWithPrivateConstructor key = TestObjectWithPrivateConstructor.randomObject(rnd);
-        final TestObjectWithPrivateConstructor val = TestObjectWithPrivateConstructor.randomObject(rnd);
+        final PrivateTestObject key = PrivateTestObject.randomObject(rnd);
+        final PrivateTestObject val = PrivateTestObject.randomObject(rnd);
         
         BinaryRow row = marshaller.marshal(key, objFactory.create());
         
@@ -520,6 +518,46 @@ public class KvMarshallerTest {
                 .defineClass(classDef, Object.class);
     }
     
+    private Column[] columnsAllTypes() {
+        Column[] cols = new Column[]{
+                new Column("primitiveByteCol", INT8, false, () -> (byte) 0x42),
+                new Column("primitiveShortCol", INT16, false, () -> (short) 0x4242),
+                new Column("primitiveIntCol", INT32, false, () -> 0x42424242),
+                new Column("primitiveLongCol", INT64, false),
+                new Column("primitiveFloatCol", FLOAT, false),
+                new Column("primitiveDoubleCol", DOUBLE, false),
+                
+                new Column("byteCol", INT8, true),
+                new Column("shortCol", INT16, true),
+                new Column("intCol", INT32, true),
+                new Column("longCol", INT64, true),
+                new Column("nullLongCol", INT64, true),
+                new Column("floatCol", FLOAT, true),
+                new Column("doubleCol", DOUBLE, true),
+                
+                new Column("dateCol", DATE, true),
+                new Column("timeCol", time(), true),
+                new Column("dateTimeCol", datetime(), true),
+                new Column("timestampCol", timestamp(), true),
+                
+                new Column("uuidCol", UUID, true),
+                new Column("bitmaskCol", NativeTypes.bitmaskOf(42), true),
+                new Column("stringCol", STRING, true),
+                new Column("nullBytesCol", BYTES, true),
+                new Column("bytesCol", BYTES, true),
+                new Column("numberCol", NativeTypes.numberOf(12), true),
+                new Column("decimalCol", NativeTypes.decimalOf(19, 3), true),
+        };
+        // Validate all types are tested.
+        Set<NativeTypeSpec> testedTypes = Arrays.stream(cols).map(c -> c.type().spec())
+                .collect(Collectors.toSet());
+        Set<NativeTypeSpec> missedTypes = Arrays.stream(NativeTypeSpec.values())
+                .filter(t -> !testedTypes.contains(t)).collect(Collectors.toSet());
+        
+        assertEquals(Collections.emptySet(), missedTypes);
+        return cols;
+    }
+    
     /**
      * Test object.
      */
@@ -656,43 +694,52 @@ public class KvMarshallerTest {
         }
     }
     
-    private Column[] columnsAllTypes() {
-        Column[] cols = new Column[]{
-                new Column("primitiveByteCol", INT8, false, () -> (byte) 0x42),
-                new Column("primitiveShortCol", INT16, false, () -> (short) 0x4242),
-                new Column("primitiveIntCol", INT32, false, () -> 0x42424242),
-                new Column("primitiveLongCol", INT64, false),
-                new Column("primitiveFloatCol", FLOAT, false),
-                new Column("primitiveDoubleCol", DOUBLE, false),
-                
-                new Column("byteCol", INT8, true),
-                new Column("shortCol", INT16, true),
-                new Column("intCol", INT32, true),
-                new Column("longCol", INT64, true),
-                new Column("nullLongCol", INT64, true),
-                new Column("floatCol", FLOAT, true),
-                new Column("doubleCol", DOUBLE, true),
-                
-                new Column("dateCol", DATE, true),
-                new Column("timeCol", time(), true),
-                new Column("dateTimeCol", datetime(), true),
-                new Column("timestampCol", timestamp(), true),
-                
-                new Column("uuidCol", UUID, true),
-                new Column("bitmaskCol", NativeTypes.bitmaskOf(42), true),
-                new Column("stringCol", STRING, true),
-                new Column("nullBytesCol", BYTES, true),
-                new Column("bytesCol", BYTES, true),
-                new Column("numberCol", NativeTypes.numberOf(12), true),
-                new Column("decimalCol", NativeTypes.decimalOf(19, 3), true),
-        };
-        // Validate all types are tested.
-        Set<NativeTypeSpec> testedTypes = Arrays.stream(cols).map(c -> c.type().spec())
-                .collect(Collectors.toSet());
-        Set<NativeTypeSpec> missedTypes = Arrays.stream(NativeTypeSpec.values())
-                .filter(t -> !testedTypes.contains(t)).collect(Collectors.toSet());
+    /**
+     * Test object without default constructor.
+     */
+    @SuppressWarnings("InstanceVariableMayNotBeInitialized")
+    private static class PrivateTestObject {
+        /**
+         * Get random TestObject.
+         */
+        static PrivateTestObject randomObject(Random rnd) {
+            return new PrivateTestObject(rnd.nextInt());
+        }
         
-        assertEquals(Collections.emptySet(), missedTypes);
-        return cols;
+        /** Value. */
+        private long primLongCol;
+        
+        /** Constructor. */
+        PrivateTestObject() {
+        }
+        
+        /**
+         * Private constructor.
+         */
+        PrivateTestObject(long val) {
+            primLongCol = val;
+        }
+        
+        /** {@inheritDoc} */
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+            
+            if (o == null || getClass() != o.getClass()) {
+                return false;
+            }
+            
+            PrivateTestObject object = (PrivateTestObject) o;
+            
+            return primLongCol == object.primLongCol;
+        }
+        
+        /** {@inheritDoc} */
+        @Override
+        public int hashCode() {
+            return Objects.hash(primLongCol);
+        }
     }
 }
diff --git a/modules/schema/src/test/java/org/apache/ignite/internal/schema/marshaller/MapperTest.java b/modules/schema/src/test/java/org/apache/ignite/internal/schema/marshaller/MapperTest.java
new file mode 100644
index 0000000..1f1e90c
--- /dev/null
+++ b/modules/schema/src/test/java/org/apache/ignite/internal/schema/marshaller/MapperTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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.internal.schema.marshaller;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.apache.ignite.table.mapper.Mapper;
+import org.apache.ignite.table.mapper.MapperBuilder;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Columns mappers test.
+ */
+public class MapperTest {
+    
+    @Test
+    public void misleadingMapping() {
+        // Empty mapping.
+        assertThrows(IllegalStateException.class, () -> Mapper.builderFor(TestObject.class).build());
+        
+        // Many fields to one column.
+        assertThrows(IllegalArgumentException.class, () -> Mapper.builderFor(TestObject.class)
+                .map("id", "key")
+                .map("longCol", "key"));
+        
+        // One field to many columns.
+        assertThrows(IllegalStateException.class, () -> Mapper.builderFor(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)
+                    .map("id", "key");
+            
+            builder.build();
+            
+            builder.map("stringCol", "val2");
+        });
+    }
+    
+    @Test
+    public void identityMapping() {
+        Mapper.identity(TestObject.class);
+    }
+    
+    /**
+     * Test object.
+     */
+    @SuppressWarnings({"InstanceVariableMayNotBeInitialized", "unused"})
+    public static class TestObject {
+        private long id;
+        
+        private long longCol;
+        
+        private String stringCol;
+    }
+}
diff --git a/modules/schema/src/test/java/org/apache/ignite/internal/schema/marshaller/KvMarshallerTest.java b/modules/schema/src/test/java/org/apache/ignite/internal/schema/marshaller/RecordMarshallerTest.java
similarity index 50%
copy from modules/schema/src/test/java/org/apache/ignite/internal/schema/marshaller/KvMarshallerTest.java
copy to modules/schema/src/test/java/org/apache/ignite/internal/schema/marshaller/RecordMarshallerTest.java
index 87b1c96..c8ea50f 100644
--- a/modules/schema/src/test/java/org/apache/ignite/internal/schema/marshaller/KvMarshallerTest.java
+++ b/modules/schema/src/test/java/org/apache/ignite/internal/schema/marshaller/RecordMarshallerTest.java
@@ -30,13 +30,10 @@ import static org.apache.ignite.internal.schema.NativeTypes.UUID;
 import static org.apache.ignite.internal.schema.NativeTypes.datetime;
 import static org.apache.ignite.internal.schema.NativeTypes.time;
 import static org.apache.ignite.internal.schema.NativeTypes.timestamp;
-import static org.junit.jupiter.api.Assertions.assertArrayEquals;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.junit.jupiter.api.DynamicContainer.dynamicContainer;
-import static org.junit.jupiter.api.DynamicTest.dynamicTest;
 
 import com.facebook.presto.bytecode.Access;
 import com.facebook.presto.bytecode.BytecodeBlock;
@@ -47,6 +44,7 @@ import com.facebook.presto.bytecode.MethodDefinition;
 import com.facebook.presto.bytecode.ParameterizedType;
 import com.facebook.presto.bytecode.Variable;
 import com.facebook.presto.bytecode.expression.BytecodeExpressions;
+import java.lang.reflect.Field;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.EnumSet;
@@ -59,11 +57,9 @@ import java.util.stream.Stream;
 import javax.annotation.processing.Generated;
 import org.apache.ignite.internal.schema.BinaryRow;
 import org.apache.ignite.internal.schema.Column;
-import org.apache.ignite.internal.schema.NativeType;
 import org.apache.ignite.internal.schema.NativeTypeSpec;
 import org.apache.ignite.internal.schema.NativeTypes;
 import org.apache.ignite.internal.schema.SchemaDescriptor;
-import org.apache.ignite.internal.schema.SchemaTestUtils;
 import org.apache.ignite.internal.schema.marshaller.reflection.ReflectionMarshallerFactory;
 import org.apache.ignite.internal.schema.row.Row;
 import org.apache.ignite.internal.schema.testobjects.TestObjectWithAllTypes;
@@ -74,17 +70,16 @@ import org.apache.ignite.internal.util.ObjectFactory;
 import org.apache.ignite.lang.IgniteInternalException;
 import org.apache.ignite.table.mapper.Mapper;
 import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.DynamicNode;
-import org.junit.jupiter.api.TestFactory;
+import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.MethodSource;
 
 /**
- * KvMarshaller test.
+ * RecordMarshaller test.
  */
-public class KvMarshallerTest {
+public class RecordMarshallerTest {
     /**
-     * Return list of marshaller factories for test.
+     * Returns list of marshaller factories for the test.
      */
     private static List<MarshallerFactory> marshallerFactoryProvider() {
         return List.of(new ReflectionMarshallerFactory());
@@ -94,7 +89,7 @@ public class KvMarshallerTest {
     private Random rnd;
     
     /**
-     * Init test.
+     * Init random.
      */
     @BeforeEach
     public void initRandom() {
@@ -105,190 +100,129 @@ public class KvMarshallerTest {
         rnd = new Random(seed);
     }
     
-    @TestFactory
-    public Stream<DynamicNode> basicTypes() {
-        NativeType[] types = new NativeType[]{INT8, INT16, INT32, INT64, FLOAT, DOUBLE, UUID, STRING, BYTES,
-                NativeTypes.bitmaskOf(5), NativeTypes.numberOf(42), NativeTypes.decimalOf(12, 3)};
-        
-        return marshallerFactoryProvider().stream().map(factory ->
-                dynamicContainer(
-                        factory.getClass().getSimpleName(),
-                        Stream.concat(
-                                // Test pure types.
-                                Stream.of(types).map(type ->
-                                        dynamicTest("testBasicTypes(" + type.spec().name() + ')', () -> checkBasicType(factory, type, type))
-                                ),
-                                
-                                // Test pairs of mixed types.
-                                Stream.of(
-                                        dynamicTest("testMixTypes 1", () -> checkBasicType(factory, INT64, INT32)),
-                                        dynamicTest("testMixTypes 1", () -> checkBasicType(factory, FLOAT, DOUBLE)),
-                                        dynamicTest("testMixTypes 1", () -> checkBasicType(factory, INT32, BYTES)),
-                                        dynamicTest("testMixTypes 1", () -> checkBasicType(factory, STRING, INT64)),
-                                        dynamicTest("testMixTypes 1", () -> checkBasicType(factory, NativeTypes.bitmaskOf(9), BYTES)),
-                                        dynamicTest("testMixTypes 1", () -> checkBasicType(factory, NativeTypes.numberOf(12), BYTES)),
-                                        dynamicTest("testMixTypes 1", () -> checkBasicType(factory, NativeTypes.decimalOf(12, 3), BYTES))
-                                )
-                        )
-                ));
-    }
-    
     @ParameterizedTest
     @MethodSource("marshallerFactoryProvider")
-    public void pojoWithFieldsOfAllTypes(MarshallerFactory factory) throws MarshallerException {
-        Column[] cols = columnsAllTypes();
-        
-        SchemaDescriptor schema = new SchemaDescriptor(1, cols, cols);
+    public void complexType(MarshallerFactory factory) throws MarshallerException {
+        SchemaDescriptor schema = new SchemaDescriptor(1, keyColumns(), valueColumnsAllTypes());
         
-        final TestObjectWithAllTypes key = TestObjectWithAllTypes.randomObject(rnd);
-        final TestObjectWithAllTypes val = TestObjectWithAllTypes.randomObject(rnd);
+        final TestObjectWithAllTypes rec = TestObjectWithAllTypes.randomObject(rnd);
         
-        KvMarshaller<TestObjectWithAllTypes, TestObjectWithAllTypes> marshaller =
-                factory.create(schema, TestObjectWithAllTypes.class, TestObjectWithAllTypes.class);
+        RecordMarshaller<TestObjectWithAllTypes> marshaller = factory.create(schema, TestObjectWithAllTypes.class);
         
-        BinaryRow row = marshaller.marshal(key, val);
+        BinaryRow row = marshaller.marshal(rec);
         
-        // Try different order.
-        TestObjectWithAllTypes restoredVal = marshaller.unmarshalValue(new Row(schema, row));
-        TestObjectWithAllTypes restoredKey = marshaller.unmarshalKey(new Row(schema, row));
+        TestObjectWithAllTypes restoredRec = marshaller.unmarshal(new Row(schema, row));
         
-        assertTrue(key.getClass().isInstance(restoredKey));
-        assertTrue(val.getClass().isInstance(restoredVal));
+        assertTrue(rec.getClass().isInstance(restoredRec));
         
-        assertEquals(key, restoredKey);
-        assertEquals(val, restoredVal);
+        assertEquals(rec, restoredRec);
     }
     
     @ParameterizedTest
     @MethodSource("marshallerFactoryProvider")
-    public void narrowType(MarshallerFactory factory) throws MarshallerException {
-        Column[] cols = columnsAllTypes();
+    public void truncatedType(MarshallerFactory factory) throws MarshallerException {
+        SchemaDescriptor schema = new SchemaDescriptor(1, keyColumns(), valueColumnsAllTypes());
         
-        SchemaDescriptor schema = new SchemaDescriptor(1, cols, cols);
+        RecordMarshaller<TestTruncatedObject> marshaller = factory.create(schema, TestTruncatedObject.class);
         
-        KvMarshaller<TestTruncatedObject, TestTruncatedObject> marshaller =
-                factory.create(schema, TestTruncatedObject.class, TestTruncatedObject.class);
+        final TestTruncatedObject rec = TestTruncatedObject.randomObject(rnd);
         
-        final TestTruncatedObject key = TestTruncatedObject.randomObject(rnd);
-        final TestTruncatedObject val = TestTruncatedObject.randomObject(rnd);
+        BinaryRow row = marshaller.marshal(rec);
         
-        BinaryRow row = marshaller.marshal(key, val);
+        Object restoredRec = marshaller.unmarshal(new Row(schema, row));
         
-        // Try different order.
-        Object restoredVal = marshaller.unmarshalValue(new Row(schema, row));
-        Object restoredKey = marshaller.unmarshalKey(new Row(schema, row));
+        assertTrue(rec.getClass().isInstance(restoredRec));
         
-        assertTrue(key.getClass().isInstance(restoredKey));
-        assertTrue(val.getClass().isInstance(restoredVal));
-        
-        assertEquals(key, restoredKey);
-        assertEquals(val, restoredVal);
+        assertEquals(rec, restoredRec);
     }
     
     @ParameterizedTest
     @MethodSource("marshallerFactoryProvider")
-    public void wideType(MarshallerFactory factory) throws MarshallerException {
-        Column[] cols = new Column[]{
-                new Column("primitiveLongCol", INT64, false),
-                new Column("primitiveDoubleCol", DOUBLE, false),
-                new Column("stringCol", STRING, true),
-        };
-        
-        SchemaDescriptor schema = new SchemaDescriptor(1, cols, cols);
+    public void widerType(MarshallerFactory factory) throws MarshallerException {
+        SchemaDescriptor schema = new SchemaDescriptor(
+                1,
+                keyColumns(),
+                new Column[]{
+                        new Column("primitiveDoubleCol", DOUBLE, false),
+                        new Column("stringCol", STRING, true),
+                }
+        );
         
-        KvMarshaller<TestObjectWithAllTypes, TestObjectWithAllTypes> marshaller =
-                factory.create(schema, TestObjectWithAllTypes.class, TestObjectWithAllTypes.class);
+        RecordMarshaller<TestObjectWithAllTypes> marshaller = factory.create(schema, TestObjectWithAllTypes.class);
         
-        final TestObjectWithAllTypes key = TestObjectWithAllTypes.randomObject(rnd);
-        final TestObjectWithAllTypes val = TestObjectWithAllTypes.randomObject(rnd);
+        final TestObjectWithAllTypes rec = TestObjectWithAllTypes.randomObject(rnd);
         
-        BinaryRow row = marshaller.marshal(key, val);
+        BinaryRow row = marshaller.marshal(rec);
         
-        // Try different order.
-        TestObjectWithAllTypes restoredVal = marshaller.unmarshalValue(new Row(schema, row));
-        TestObjectWithAllTypes restoredKey = marshaller.unmarshalKey(new Row(schema, row));
+        TestObjectWithAllTypes restoredRec = marshaller.unmarshal(new Row(schema, row));
         
-        assertTrue(key.getClass().isInstance(restoredKey));
-        assertTrue(val.getClass().isInstance(restoredVal));
+        assertTrue(rec.getClass().isInstance(restoredRec));
         
-        TestObjectWithAllTypes expectedKey = new TestObjectWithAllTypes();
-        expectedKey.setPrimitiveLongCol(key.getPrimitiveLongCol());
-        expectedKey.setPrimitiveDoubleCol(key.getPrimitiveDoubleCol());
-        expectedKey.setStringCol(key.getStringCol());
+        TestObjectWithAllTypes expectedRec = new TestObjectWithAllTypes();
         
-        TestObjectWithAllTypes expectedVal = new TestObjectWithAllTypes();
-        expectedVal.setPrimitiveLongCol(val.getPrimitiveLongCol());
-        expectedVal.setPrimitiveDoubleCol(val.getPrimitiveDoubleCol());
-        expectedVal.setStringCol(val.getStringCol());
+        expectedRec.setPrimitiveLongCol(rec.getPrimitiveLongCol());
+        expectedRec.setIntCol(rec.getIntCol());
+        expectedRec.setPrimitiveDoubleCol(rec.getPrimitiveDoubleCol());
+        expectedRec.setStringCol(rec.getStringCol());
         
-        assertEquals(expectedKey, restoredKey);
-        assertEquals(expectedVal, restoredVal);
+        assertEquals(expectedRec, restoredRec);
         
-        assertNull(restoredKey.getUuidCol());
-        assertNull(restoredVal.getUuidCol());
-        assertEquals(0, restoredKey.getPrimitiveIntCol());
-        assertEquals(0, restoredVal.getPrimitiveIntCol());
+        // Check non-mapped fields has default values.
+        assertNull(restoredRec.getUuidCol());
+        assertEquals(0, restoredRec.getPrimitiveIntCol());
     }
     
     @ParameterizedTest
     @MethodSource("marshallerFactoryProvider")
-    public void columnNameMapping(MarshallerFactory factory) throws MarshallerException {
+    public void mapping(MarshallerFactory factory) throws MarshallerException {
         SchemaDescriptor schema = new SchemaDescriptor(1,
                 new Column[]{new Column("key", INT64, false)},
                 new Column[]{
-                        new Column("col1", INT64, false),
+                        new Column("col1", INT32, false),
                         new Column("col2", INT64, true),
                         new Column("col3", STRING, false)
                 });
         
-        Mapper<TestKeyObject> keyMapper = Mapper.builderFor(TestKeyObject.class)
+        Mapper<TestObject> mapper = Mapper.builderFor(TestObject.class)
                 .map("id", "key")
-                .build();
-        
-        Mapper<TestObject> valMapper = Mapper.builderFor(TestObject.class)
-                .map("longCol", "col1")
+                .map("intCol", "col1")
                 .map("stringCol", "col3")
                 .build();
         
-        KvMarshaller<TestKeyObject, TestObject> marshaller = factory.create(schema, keyMapper, valMapper);
+        RecordMarshaller<TestObject> marshaller = factory.create(schema, mapper);
         
-        final TestKeyObject key = TestKeyObject.randomObject(rnd);
-        final TestObject val = TestObject.randomObject(rnd);
+        final TestObject rec = TestObject.randomObject(rnd);
         
-        BinaryRow row = marshaller.marshal(key, val);
+        BinaryRow row = marshaller.marshal(rec);
         
-        // Try different order.
-        Object restoredVal = marshaller.unmarshalValue(new Row(schema, row));
-        Object restoredKey = marshaller.unmarshalKey(new Row(schema, row));
+        Object restoredRec = marshaller.unmarshal(new Row(schema, row));
         
-        assertTrue(key.getClass().isInstance(restoredKey));
-        assertTrue(val.getClass().isInstance(restoredVal));
+        assertTrue(rec.getClass().isInstance(restoredRec));
         
-        val.longCol2 = null;
+        rec.longCol2 = null; // Nullify non-mapped field.
         
-        assertEquals(key, restoredKey);
-        assertEquals(val, restoredVal);
+        assertEquals(rec, restoredRec);
     }
     
     @ParameterizedTest
     @MethodSource("marshallerFactoryProvider")
     public void classWithWrongFieldType(MarshallerFactory factory) {
-        Column[] cols = new Column[]{
-                new Column("bitmaskCol", NativeTypes.bitmaskOf(42), true),
-                new Column("shortCol", UUID, true)
-        };
-        
-        SchemaDescriptor schema = new SchemaDescriptor(1, cols, cols);
+        SchemaDescriptor schema = new SchemaDescriptor(
+                1,
+                keyColumns(),
+                new Column[]{
+                        new Column("bitmaskCol", NativeTypes.bitmaskOf(42), true),
+                        new Column("shortCol", UUID, true)
+                }
+        );
         
-        KvMarshaller<TestObjectWithAllTypes, TestObjectWithAllTypes> marshaller =
-                factory.create(schema, TestObjectWithAllTypes.class, TestObjectWithAllTypes.class);
+        RecordMarshaller<TestObjectWithAllTypes> marshaller = factory.create(schema, TestObjectWithAllTypes.class);
         
-        final TestObjectWithAllTypes key = TestObjectWithAllTypes.randomObject(rnd);
-        final TestObjectWithAllTypes val = TestObjectWithAllTypes.randomObject(rnd);
+        final TestObjectWithAllTypes rec = TestObjectWithAllTypes.randomObject(rnd);
         
         assertThrows(
                 MarshallerException.class,
-                () -> marshaller.marshal(key, val),
+                () -> marshaller.marshal(rec),
                 "Failed to write field [name=shortCol]"
         );
     }
@@ -296,96 +230,85 @@ public class KvMarshallerTest {
     @ParameterizedTest
     @MethodSource("marshallerFactoryProvider")
     public void classWithIncorrectBitmaskSize(MarshallerFactory factory) {
-        Column[] cols = new Column[]{
-                new Column("primitiveLongCol", INT64, false),
-                new Column("bitmaskCol", NativeTypes.bitmaskOf(9), true),
-        };
-        
-        SchemaDescriptor schema = new SchemaDescriptor(1, cols, cols);
+        SchemaDescriptor schema = new SchemaDescriptor(
+                1,
+                keyColumns(),
+                new Column[]{
+                        new Column("primitiveLongCol", INT64, false),
+                        new Column("bitmaskCol", NativeTypes.bitmaskOf(9), true),
+                }
+        );
         
-        KvMarshaller<TestObjectWithAllTypes, TestObjectWithAllTypes> marshaller =
-                factory.create(schema, TestObjectWithAllTypes.class, TestObjectWithAllTypes.class);
+        RecordMarshaller<TestObjectWithAllTypes> marshaller = factory.create(schema, TestObjectWithAllTypes.class);
         
-        final TestObjectWithAllTypes key = TestObjectWithAllTypes.randomObject(rnd);
-        final TestObjectWithAllTypes val = TestObjectWithAllTypes.randomObject(rnd);
+        final TestObjectWithAllTypes rec = TestObjectWithAllTypes.randomObject(rnd);
         
         assertThrows(
                 MarshallerException.class,
-                () -> marshaller.marshal(key, val),
+                () -> marshaller.marshal(rec),
                 "Failed to write field [name=bitmaskCol]"
         );
     }
     
     @ParameterizedTest
     @MethodSource("marshallerFactoryProvider")
-    public void classWithPrivateConstructor(MarshallerFactory factory) throws MarshallerException {
-        Column[] cols = new Column[]{
-                new Column("primLongCol", INT64, false),
-        };
-        
-        SchemaDescriptor schema = new SchemaDescriptor(1, cols, cols);
-        
-        KvMarshaller<TestObjectWithPrivateConstructor, TestObjectWithPrivateConstructor> marshaller =
-                factory.create(schema, TestObjectWithPrivateConstructor.class, TestObjectWithPrivateConstructor.class);
+    public void classWithPrivateConstructor(MarshallerFactory factory) throws MarshallerException, IllegalAccessException {
+        SchemaDescriptor schema = new SchemaDescriptor(
+                1,
+                new Column[]{new Column("primLongCol", INT64, false)},
+                new Column[]{new Column("primIntCol", INT32, false)}
+        );
         
-        final TestObjectWithPrivateConstructor key = TestObjectWithPrivateConstructor.randomObject(rnd);
-        final TestObjectWithPrivateConstructor val = TestObjectWithPrivateConstructor.randomObject(rnd);
+        RecordMarshaller<TestObjectWithPrivateConstructor> marshaller = factory.create(schema, TestObjectWithPrivateConstructor.class);
         
-        BinaryRow row = marshaller.marshal(key, val);
+        final TestObjectWithPrivateConstructor rec = TestObjectWithPrivateConstructor.randomObject(rnd);
         
-        Object key1 = marshaller.unmarshalKey(new Row(schema, row));
-        Object val1 = marshaller.unmarshalValue(new Row(schema, row));
+        BinaryRow row = marshaller.marshal(rec);
         
-        assertTrue(key.getClass().isInstance(key1));
-        assertTrue(val.getClass().isInstance(val1));
+        TestObjectWithPrivateConstructor restoredRec = marshaller.unmarshal(new Row(schema, row));
         
-        assertEquals(key, key);
-        assertEquals(val, val1);
+        assertDeepEquals(TestObjectWithPrivateConstructor.class, rec, restoredRec);
     }
     
     @ParameterizedTest
     @MethodSource("marshallerFactoryProvider")
     public void classWithNoDefaultConstructor(MarshallerFactory factory) {
-        Column[] cols = new Column[]{
-                new Column("primLongCol", INT64, false),
-        };
-        
-        SchemaDescriptor schema = new SchemaDescriptor(1, cols, cols);
+        SchemaDescriptor schema = new SchemaDescriptor(
+                1,
+                new Column[]{new Column("primLongCol", INT64, false)},
+                new Column[]{new Column("primIntCol", INT32, false)}
+        );
         
-        final Object key = TestObjectWithNoDefaultConstructor.randomObject(rnd);
-        final Object val = TestObjectWithNoDefaultConstructor.randomObject(rnd);
+        final Object rec = TestObjectWithNoDefaultConstructor.randomObject(rnd);
         
-        assertThrows(IgniteInternalException.class, () -> factory.create(schema, key.getClass(), val.getClass()));
+        assertThrows(IgniteInternalException.class, () -> factory.create(schema, rec.getClass()));
     }
-
+    
     @ParameterizedTest
     @MethodSource("marshallerFactoryProvider")
     public void privateClass(MarshallerFactory factory) throws MarshallerException {
-        Column[] cols = new Column[]{
-                new Column("primLongCol", INT64, false),
-        };
-        
-        SchemaDescriptor schema = new SchemaDescriptor(1, cols, cols);
+        SchemaDescriptor schema = new SchemaDescriptor(
+                1,
+                new Column[]{new Column("primLongCol", INT64, false)},
+                new Column[]{new Column("primIntCol", INT32, false)}
+        );
         
-        final ObjectFactory<TestObjectWithPrivateConstructor> objFactory = new ObjectFactory<>(TestObjectWithPrivateConstructor.class);
-        final KvMarshaller<TestObjectWithPrivateConstructor, TestObjectWithPrivateConstructor> marshaller =
-                factory.create(schema, TestObjectWithPrivateConstructor.class, TestObjectWithPrivateConstructor.class);
+        final ObjectFactory<PrivateTestObject> objFactory = new ObjectFactory<>(PrivateTestObject.class);
+        final RecordMarshaller<PrivateTestObject> marshaller = factory
+                .create(schema, PrivateTestObject.class);
         
-        final TestObjectWithPrivateConstructor key = TestObjectWithPrivateConstructor.randomObject(rnd);
-        final TestObjectWithPrivateConstructor val = TestObjectWithPrivateConstructor.randomObject(rnd);
+        final PrivateTestObject rec = PrivateTestObject.randomObject(rnd);
         
-        BinaryRow row = marshaller.marshal(key, objFactory.create());
+        BinaryRow row = marshaller.marshal(objFactory.create());
         
-        Object key1 = marshaller.unmarshalKey(new Row(schema, row));
-        Object val1 = marshaller.unmarshalValue(new Row(schema, row));
+        Object restoredRec = marshaller.unmarshal(new Row(schema, row));
         
-        assertTrue(key.getClass().isInstance(key1));
-        assertTrue(val.getClass().isInstance(val1));
+        assertTrue(rec.getClass().isInstance(restoredRec));
     }
     
     @ParameterizedTest
     @MethodSource("marshallerFactoryProvider")
-    public void classLoader(MarshallerFactory factory) throws MarshallerException {
+    public void classLoader(MarshallerFactory factory) throws MarshallerException, IllegalAccessException {
         final ClassLoader loader = Thread.currentThread().getContextClassLoader();
         try {
             Thread.currentThread().setContextClassLoader(new DynamicClassLoader(getClass().getClassLoader()));
@@ -402,80 +325,36 @@ public class KvMarshallerTest {
             
             SchemaDescriptor schema = new SchemaDescriptor(1, keyCols, valCols);
             
-            final Class<?> valClass = createGeneratedObjectClass();
-            final ObjectFactory<?> objFactory = new ObjectFactory<>(valClass);
-            
-            KvMarshaller<Long, Object> marshaller = factory.create(schema, Long.class, (Class<Object>) valClass);
+            final Class<Object> recClass = (Class<Object>) createGeneratedObjectClass();
+            final ObjectFactory<Object> objFactory = new ObjectFactory<>(recClass);
             
-            final Long key = rnd.nextLong();
+            RecordMarshaller<Object> marshaller = factory.create(schema, recClass);
             
-            BinaryRow row = marshaller.marshal(key, objFactory.create());
+            Object rec = objFactory.create();
             
-            Long key1 = marshaller.unmarshalKey(new Row(schema, row));
-            Object val1 = marshaller.unmarshalValue(new Row(schema, row));
+            BinaryRow row = marshaller.marshal(rec);
             
-            assertTrue(valClass.isInstance(val1));
+            Object restoredRec = marshaller.unmarshal(new Row(schema, row));
             
-            assertEquals(key, key1);
+            assertDeepEquals(recClass, rec, restoredRec);
         } finally {
             Thread.currentThread().setContextClassLoader(loader);
         }
     }
     
     /**
-     * Generate random key-value pair of given types and check serialization and deserialization works fine.
-     *
-     * @param factory KvMarshaller factory.
-     * @param keyType Key type.
-     * @param valType Value type.
-     * @throws MarshallerException If (de)serialization failed.
+     * Validate all types are tested.
      */
-    private void checkBasicType(MarshallerFactory factory, NativeType keyType,
-            NativeType valType) throws MarshallerException {
-        final Object key = generateRandomValue(keyType);
-        final Object val = generateRandomValue(valType);
-        
-        Column[] keyCols = new Column[]{new Column("key", keyType, false)};
-        Column[] valCols = new Column[]{new Column("val", valType, false)};
-        
-        SchemaDescriptor schema = new SchemaDescriptor(1, keyCols, valCols);
-        
-        KvMarshaller<Object, Object> marshaller = factory.create(schema, (Class<Object>) key.getClass(), (Class<Object>) val.getClass());
-        
-        BinaryRow row = marshaller.marshal(key, val);
-        
-        Object key1 = marshaller.unmarshalKey(new Row(schema, row));
-        Object val1 = marshaller.unmarshalValue(new Row(schema, row));
+    @Test
+    public void ensureAllTypesChecked() {
+        Set<NativeTypeSpec> testedTypes = Stream.concat(Arrays.stream(keyColumns()), Arrays.stream(valueColumnsAllTypes()))
+                .map(c -> c.type().spec())
+                .collect(Collectors.toSet());
         
-        assertTrue(key.getClass().isInstance(key1));
-        assertTrue(val.getClass().isInstance(val1));
+        Set<NativeTypeSpec> missedTypes = Arrays.stream(NativeTypeSpec.values())
+                .filter(t -> !testedTypes.contains(t)).collect(Collectors.toSet());
         
-        compareObjects(keyType, key, key);
-        compareObjects(valType, val, val1);
-    }
-    
-    /**
-     * Compare object regarding NativeType.
-     *
-     * @param type Native type.
-     * @param exp  Expected value.
-     * @param act  Actual value.
-     */
-    private void compareObjects(NativeType type, Object exp, Object act) {
-        if (type.spec() == NativeTypeSpec.BYTES) {
-            assertArrayEquals((byte[]) exp, (byte[]) act);
-        } else {
-            assertEquals(exp, act);
-        }
-    }
-    
-    /**
-     * Generates random value of given type.
-     *
-     * @param type Type.
-     */
-    private Object generateRandomValue(NativeType type) {
-        return SchemaTestUtils.generateRandomValue(rnd, type);
+        assertEquals(Collections.emptySet(), missedTypes);
     }
     
     /**
@@ -494,6 +373,8 @@ public class KvMarshallerTest {
         );
         classDef.declareAnnotation(Generated.class).setValue("value", getClass().getCanonicalName());
         
+        classDef.declareField(EnumSet.of(Access.PRIVATE), "key", ParameterizedType.type(long.class));
+        
         for (int i = 0; i < 3; i++) {
             classDef.declareField(EnumSet.of(Access.PRIVATE), "col" + i, ParameterizedType.type(long.class));
         }
@@ -507,6 +388,8 @@ public class KvMarshallerTest {
                 .invokeConstructor(classDef.getSuperClass())
                 .append(rnd.set(BytecodeExpressions.newInstance(Random.class)));
         
+        body.append(methodDef.getThis().setField("key", rnd.invoke("nextLong", long.class).cast(long.class)));
+        
         for (int i = 0; i < 3; i++) {
             body.append(methodDef.getThis().setField("col" + i, rnd.invoke("nextLong", long.class).cast(long.class)));
         }
@@ -520,39 +403,51 @@ public class KvMarshallerTest {
                 .defineClass(classDef, Object.class);
     }
     
-    /**
-     * Test object.
-     */
-    @SuppressWarnings("InstanceVariableMayNotBeInitialized")
-    public static class TestKeyObject {
-        static TestKeyObject randomObject(Random rnd) {
-            final TestKeyObject obj = new TestKeyObject();
-            
-            obj.id = rnd.nextLong();
-            
-            return obj;
-        }
-        
-        private long id;
-        
-        @Override
-        public boolean equals(Object o) {
-            if (this == o) {
-                return true;
-            }
-            if (o == null || getClass() != o.getClass()) {
-                return false;
-            }
-            TestKeyObject that = (TestKeyObject) o;
-            return id == that.id;
-        }
+    private <T> void assertDeepEquals(Class<T> recClass, T rec, T restoredRec) throws IllegalAccessException {
+        assertTrue(recClass.isInstance(restoredRec));
         
-        @Override
-        public int hashCode() {
-            return Objects.hash(id);
+        for (Field fld : recClass.getDeclaredFields()) {
+            fld.setAccessible(true);
+            assertEquals(fld.get(rec), fld.get(restoredRec), fld.getName());
         }
     }
     
+    private Column[] keyColumns() {
+        return new Column[]{
+                new Column("primitiveLongCol", INT64, false),
+                new Column("intCol", INT32, true)
+        };
+    }
+    
+    private Column[] valueColumnsAllTypes() {
+        return new Column[]{
+                new Column("primitiveByteCol", INT8, false, () -> (byte) 0x42),
+                new Column("primitiveShortCol", INT16, false, () -> (short) 0x4242),
+                new Column("primitiveIntCol", INT32, false, () -> 0x42424242),
+                new Column("primitiveFloatCol", FLOAT, false),
+                new Column("primitiveDoubleCol", DOUBLE, false),
+                
+                new Column("byteCol", INT8, true),
+                new Column("shortCol", INT16, true),
+                new Column("longCol", INT64, true),
+                new Column("nullLongCol", INT64, true),
+                new Column("floatCol", FLOAT, true),
+                new Column("doubleCol", DOUBLE, true),
+                
+                new Column("dateCol", DATE, true),
+                new Column("timeCol", time(), true),
+                new Column("dateTimeCol", datetime(), true),
+                new Column("timestampCol", timestamp(), true),
+                
+                new Column("uuidCol", UUID, true),
+                new Column("bitmaskCol", NativeTypes.bitmaskOf(42), true),
+                new Column("stringCol", STRING, true),
+                new Column("nullBytesCol", BYTES, true),
+                new Column("bytesCol", BYTES, true),
+                new Column("numberCol", NativeTypes.numberOf(12), true),
+                new Column("decimalCol", NativeTypes.decimalOf(19, 3), true),
+        };
+    }
     
     /**
      * Test object.
@@ -562,14 +457,17 @@ public class KvMarshallerTest {
         static TestObject randomObject(Random rnd) {
             final TestObject obj = new TestObject();
             
-            obj.longCol = rnd.nextLong();
+            obj.id = rnd.nextLong();
+            obj.intCol = rnd.nextInt();
             obj.longCol2 = rnd.nextLong();
             obj.stringCol = IgniteTestUtils.randomString(rnd, 100);
             
             return obj;
         }
         
-        private long longCol;
+        private long id;
+        
+        private int intCol;
         
         private Long longCol2;
         
@@ -587,19 +485,20 @@ public class KvMarshallerTest {
             
             TestObject that = (TestObject) o;
             
-            return longCol == that.longCol
+            return id == that.id
+                    && intCol == that.intCol
                     && Objects.equals(longCol2, that.longCol2)
                     && Objects.equals(stringCol, that.stringCol);
         }
         
         @Override
         public int hashCode() {
-            return Objects.hash(longCol, longCol2, stringCol);
+            return Objects.hash(id);
         }
     }
     
     /**
-     * Test object.
+     * Test object with less amount of fields.
      */
     @SuppressWarnings("InstanceVariableMayNotBeInitialized")
     public static class TestTruncatedObject {
@@ -629,7 +528,6 @@ public class KvMarshallerTest {
         
         private java.util.UUID uuidCol;
         
-        /** {@inheritDoc} */
         @Override
         public boolean equals(Object o) {
             if (this == o) {
@@ -649,50 +547,51 @@ public class KvMarshallerTest {
                     && Objects.equals(uuidCol, ((TestTruncatedObject) o).uuidCol);
         }
         
-        /** {@inheritDoc} */
         @Override
         public int hashCode() {
             return 42;
         }
     }
     
-    private Column[] columnsAllTypes() {
-        Column[] cols = new Column[]{
-                new Column("primitiveByteCol", INT8, false, () -> (byte) 0x42),
-                new Column("primitiveShortCol", INT16, false, () -> (short) 0x4242),
-                new Column("primitiveIntCol", INT32, false, () -> 0x42424242),
-                new Column("primitiveLongCol", INT64, false),
-                new Column("primitiveFloatCol", FLOAT, false),
-                new Column("primitiveDoubleCol", DOUBLE, false),
-                
-                new Column("byteCol", INT8, true),
-                new Column("shortCol", INT16, true),
-                new Column("intCol", INT32, true),
-                new Column("longCol", INT64, true),
-                new Column("nullLongCol", INT64, true),
-                new Column("floatCol", FLOAT, true),
-                new Column("doubleCol", DOUBLE, true),
-                
-                new Column("dateCol", DATE, true),
-                new Column("timeCol", time(), true),
-                new Column("dateTimeCol", datetime(), true),
-                new Column("timestampCol", timestamp(), true),
-                
-                new Column("uuidCol", UUID, true),
-                new Column("bitmaskCol", NativeTypes.bitmaskOf(42), true),
-                new Column("stringCol", STRING, true),
-                new Column("nullBytesCol", BYTES, true),
-                new Column("bytesCol", BYTES, true),
-                new Column("numberCol", NativeTypes.numberOf(12), true),
-                new Column("decimalCol", NativeTypes.decimalOf(19, 3), true),
-        };
-        // Validate all types are tested.
-        Set<NativeTypeSpec> testedTypes = Arrays.stream(cols).map(c -> c.type().spec())
-                .collect(Collectors.toSet());
-        Set<NativeTypeSpec> missedTypes = Arrays.stream(NativeTypeSpec.values())
-                .filter(t -> !testedTypes.contains(t)).collect(Collectors.toSet());
+    /**
+     * Test object without default constructor.
+     */
+    @SuppressWarnings("InstanceVariableMayNotBeInitialized")
+    private static class PrivateTestObject {
+        static PrivateTestObject randomObject(Random rnd) {
+            return new PrivateTestObject(rnd.nextLong(), rnd.nextInt());
+        }
         
-        assertEquals(Collections.emptySet(), missedTypes);
-        return cols;
+        private long primLongCol;
+        
+        private int primIntCol;
+        
+        PrivateTestObject() {
+        }
+        
+        PrivateTestObject(long longVal, int intVal) {
+            primLongCol = longVal;
+            primIntCol = intVal;
+        }
+        
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+            
+            if (o == null || getClass() != o.getClass()) {
+                return false;
+            }
+            
+            PrivateTestObject object = (PrivateTestObject) o;
+            
+            return primLongCol == object.primLongCol && primIntCol == object.primIntCol;
+        }
+        
+        @Override
+        public int hashCode() {
+            return Objects.hash(primLongCol);
+        }
     }
 }
diff --git a/modules/schema/src/test/java/org/apache/ignite/internal/schema/testobjects/TestObjectWithNoDefaultConstructor.java b/modules/schema/src/test/java/org/apache/ignite/internal/schema/testobjects/TestObjectWithNoDefaultConstructor.java
index b26230e..f529940 100644
--- a/modules/schema/src/test/java/org/apache/ignite/internal/schema/testobjects/TestObjectWithNoDefaultConstructor.java
+++ b/modules/schema/src/test/java/org/apache/ignite/internal/schema/testobjects/TestObjectWithNoDefaultConstructor.java
@@ -28,25 +28,28 @@ public class TestObjectWithNoDefaultConstructor {
      * Creates an object with random data.
      */
     public static TestObjectWithNoDefaultConstructor randomObject(Random rnd) {
-        return new TestObjectWithNoDefaultConstructor(rnd.nextLong());
+        return new TestObjectWithNoDefaultConstructor(rnd.nextLong(), rnd.nextInt());
     }
     
-    /** Value. */
     private final long primLongCol;
     
+    private final int primIntCol;
+    
     /**
      * Private constructor.
      */
-    public TestObjectWithNoDefaultConstructor(long val) {
-        primLongCol = val;
+    public TestObjectWithNoDefaultConstructor(long longVal, int intVal) {
+        primLongCol = longVal;
+        primIntCol = intVal;
     }
     
     /** {@inheritDoc} */
-    @Override public boolean equals(Object o) {
+    @Override
+    public boolean equals(Object o) {
         if (this == o) {
             return true;
         }
-    
+        
         if (o == null || getClass() != o.getClass()) {
             return false;
         }
diff --git a/modules/schema/src/test/java/org/apache/ignite/internal/schema/testobjects/TestObjectWithPrivateConstructor.java b/modules/schema/src/test/java/org/apache/ignite/internal/schema/testobjects/TestObjectWithPrivateConstructor.java
index 291f301..e6c0898 100644
--- a/modules/schema/src/test/java/org/apache/ignite/internal/schema/testobjects/TestObjectWithPrivateConstructor.java
+++ b/modules/schema/src/test/java/org/apache/ignite/internal/schema/testobjects/TestObjectWithPrivateConstructor.java
@@ -32,13 +32,15 @@ public class TestObjectWithPrivateConstructor {
         final TestObjectWithPrivateConstructor obj = new TestObjectWithPrivateConstructor();
         
         obj.primLongCol = rnd.nextLong();
+        obj.primIntCol = rnd.nextInt();
         
         return obj;
     }
     
-    /** Value. */
     private long primLongCol;
     
+    private int primIntCol;
+    
     /**
      * Private constructor.
      */
@@ -46,11 +48,12 @@ public class TestObjectWithPrivateConstructor {
     }
     
     /** {@inheritDoc} */
-    @Override public boolean equals(Object o) {
+    @Override
+    public boolean equals(Object o) {
         if (this == o) {
             return true;
         }
-    
+        
         if (o == null || getClass() != o.getClass()) {
             return false;
         }
diff --git a/modules/table/src/main/java/org/apache/ignite/internal/table/KeyValueViewImpl.java b/modules/table/src/main/java/org/apache/ignite/internal/table/KeyValueViewImpl.java
index 118449b..f9c1f57 100644
--- a/modules/table/src/main/java/org/apache/ignite/internal/table/KeyValueViewImpl.java
+++ b/modules/table/src/main/java/org/apache/ignite/internal/table/KeyValueViewImpl.java
@@ -42,15 +42,11 @@ import org.jetbrains.annotations.Nullable;
  * Key-value view implementation.
  */
 public class KeyValueViewImpl<K, V> extends AbstractTableView implements KeyValueView<K, V> {
-    /**
-     * Marshaller factory.
-     */
-    private final Function<SchemaDescriptor, KvMarshallerImpl<K, V>> marshallerFactory;
+    /** Marshaller factory. */
+    private final Function<SchemaDescriptor, KvMarshaller<K, V>> marshallerFactory;
     
-    /**
-     * Marshaller.
-     */
-    private KvMarshallerImpl<K, V> marsh;
+    /** Key-value marshaller. */
+    private KvMarshaller<K, V> marsh;
     
     /**
      * Constructor.
diff --git a/modules/table/src/main/java/org/apache/ignite/internal/table/RecordViewImpl.java b/modules/table/src/main/java/org/apache/ignite/internal/table/RecordViewImpl.java
index 22e410d..9b89a6a 100644
--- a/modules/table/src/main/java/org/apache/ignite/internal/table/RecordViewImpl.java
+++ b/modules/table/src/main/java/org/apache/ignite/internal/table/RecordViewImpl.java
@@ -22,11 +22,15 @@ import java.util.Collection;
 import java.util.Map;
 import java.util.Objects;
 import java.util.concurrent.CompletableFuture;
+import java.util.function.Function;
 import org.apache.ignite.internal.schema.BinaryRow;
 import org.apache.ignite.internal.schema.SchemaDescriptor;
 import org.apache.ignite.internal.schema.SchemaRegistry;
-import org.apache.ignite.internal.schema.marshaller.RecordSerializer;
+import org.apache.ignite.internal.schema.marshaller.MarshallerException;
+import org.apache.ignite.internal.schema.marshaller.RecordMarshaller;
+import org.apache.ignite.internal.schema.marshaller.reflection.RecordMarshallerImpl;
 import org.apache.ignite.internal.schema.row.Row;
+import org.apache.ignite.lang.IgniteException;
 import org.apache.ignite.table.InvokeProcessor;
 import org.apache.ignite.table.RecordView;
 import org.apache.ignite.table.mapper.Mapper;
@@ -38,6 +42,12 @@ import org.jetbrains.annotations.Nullable;
  * Record view implementation.
  */
 public class RecordViewImpl<R> extends AbstractTableView implements RecordView<R> {
+    /** Marshaller factory. */
+    private final Function<SchemaDescriptor, RecordMarshaller<R>> marshallerFactory;
+    
+    /** Record marshaller. */
+    private RecordMarshaller<R> marsh;
+    
     /**
      * Constructor.
      *
@@ -48,202 +58,222 @@ public class RecordViewImpl<R> extends AbstractTableView implements RecordView<R
      */
     public RecordViewImpl(InternalTable tbl, SchemaRegistry schemaReg, Mapper<R> mapper, @Nullable Transaction tx) {
         super(tbl, schemaReg, tx);
+        
+        marshallerFactory = (schema) -> new RecordMarshallerImpl<>(schema, mapper);
     }
-
+    
     /** {@inheritDoc} */
     @Override
     public R get(@NotNull R keyRec) {
         return sync(getAsync(keyRec));
     }
-
+    
     /** {@inheritDoc} */
     @Override
     public @NotNull CompletableFuture<R> getAsync(@NotNull R keyRec) {
         Objects.requireNonNull(keyRec);
-
-        RecordSerializer<R> marsh = serializer();
-
-        Row keyRow = marsh.serialize(keyRec);  // Convert to portable format to pass TX/storage layer.
-
+        
+        BinaryRow keyRow = marshalKey(keyRec);  // Convert to portable format to pass TX/storage layer.
+        
         return tbl.get(keyRow, tx)  // Load async.
                 .thenApply(this::wrap) // Binary -> schema-aware row
-                .thenApply(marsh::deserialize); // Deserialize.
+                .thenApply(this::unmarshal); // Deserialize.
     }
-
+    
     /** {@inheritDoc} */
     @Override
     public Collection<R> getAll(@NotNull Collection<R> keyRecs) {
-        throw new UnsupportedOperationException("Not implemented yet.");
+        return sync(getAllAsync(keyRecs));
     }
-
+    
     /** {@inheritDoc} */
     @Override
     public @NotNull CompletableFuture<Collection<R>> getAllAsync(@NotNull Collection<R> keyRecs) {
         throw new UnsupportedOperationException("Not implemented yet.");
     }
-
+    
     /** {@inheritDoc} */
     @Override
     public void upsert(@NotNull R rec) {
-        throw new UnsupportedOperationException("Not implemented yet.");
+        sync(upsertAsync(rec));
     }
-
+    
     /** {@inheritDoc} */
     @Override
     public @NotNull CompletableFuture<Void> upsertAsync(@NotNull R rec) {
-        throw new UnsupportedOperationException("Not implemented yet.");
+        BinaryRow keyRow = marshal(Objects.requireNonNull(rec));
+        
+        return tbl.upsert(keyRow, tx).thenAccept(ignore -> {
+        });
     }
-
+    
     /** {@inheritDoc} */
     @Override
     public void upsertAll(@NotNull Collection<R> recs) {
-        throw new UnsupportedOperationException("Not implemented yet.");
+        sync(upsertAllAsync(recs));
     }
-
+    
     /** {@inheritDoc} */
     @Override
     public @NotNull CompletableFuture<Void> upsertAllAsync(@NotNull Collection<R> recs) {
         throw new UnsupportedOperationException("Not implemented yet.");
     }
-
+    
     /** {@inheritDoc} */
     @Override
     public R getAndUpsert(@NotNull R rec) {
-        throw new UnsupportedOperationException("Not implemented yet.");
+        return sync(getAndUpsertAsync(rec));
     }
-
+    
     /** {@inheritDoc} */
     @Override
     public @NotNull CompletableFuture<R> getAndUpsertAsync(@NotNull R rec) {
-        throw new UnsupportedOperationException("Not implemented yet.");
+        BinaryRow keyRow = marshal(Objects.requireNonNull(rec));
+    
+        return tbl.getAndUpsert(keyRow, tx).thenApply(this::unmarshal);
     }
-
+    
     /** {@inheritDoc} */
     @Override
     public boolean insert(@NotNull R rec) {
-        throw new UnsupportedOperationException("Not implemented yet.");
+        return sync(insertAsync(rec));
     }
-
+    
     /** {@inheritDoc} */
     @Override
     public @NotNull CompletableFuture<Boolean> insertAsync(@NotNull R rec) {
-        throw new UnsupportedOperationException("Not implemented yet.");
+        BinaryRow keyRow = marshal(Objects.requireNonNull(rec));
+    
+        return tbl.insert(keyRow, tx);
     }
-
+    
     /** {@inheritDoc} */
     @Override
     public Collection<R> insertAll(@NotNull Collection<R> recs) {
-        throw new UnsupportedOperationException("Not implemented yet.");
+        return sync(insertAllAsync(recs));
     }
-
+    
     /** {@inheritDoc} */
     @Override
     public @NotNull CompletableFuture<Collection<R>> insertAllAsync(@NotNull Collection<R> recs) {
         throw new UnsupportedOperationException("Not implemented yet.");
     }
-
+    
     /** {@inheritDoc} */
     @Override
     public boolean replace(@NotNull R rec) {
-        throw new UnsupportedOperationException("Not implemented yet.");
+        return sync(replaceAsync(rec));
     }
-
+    
     /** {@inheritDoc} */
     @Override
     public boolean replace(@NotNull R oldRec, @NotNull R newRec) {
-        throw new UnsupportedOperationException("Not implemented yet.");
+        return sync(replaceAsync(oldRec, newRec));
     }
-
+    
     /** {@inheritDoc} */
     @Override
     public @NotNull CompletableFuture<Boolean> replaceAsync(@NotNull R rec) {
-        throw new UnsupportedOperationException("Not implemented yet.");
+        BinaryRow newRow = marshal(rec);
+    
+        return tbl.replace(newRow, tx);
     }
-
+    
     /** {@inheritDoc} */
     @Override
     public @NotNull CompletableFuture<Boolean> replaceAsync(@NotNull R oldRec, @NotNull R newRec) {
-        throw new UnsupportedOperationException("Not implemented yet.");
+        BinaryRow oldRow = marshal(oldRec);
+        BinaryRow newRow = marshal(newRec);
+    
+        return tbl.replace(oldRow, newRow, tx);
     }
-
+    
     /** {@inheritDoc} */
     @Override
     public R getAndReplace(@NotNull R rec) {
-        throw new UnsupportedOperationException("Not implemented yet.");
+        return sync(getAndReplaceAsync(rec));
     }
-
+    
     /** {@inheritDoc} */
     @Override
     public @NotNull CompletableFuture<R> getAndReplaceAsync(@NotNull R rec) {
-        throw new UnsupportedOperationException("Not implemented yet.");
+        BinaryRow row = marshal(rec);
+    
+        return tbl.getAndReplace(row, tx).thenApply(this::unmarshal);
     }
-
+    
     /** {@inheritDoc} */
     @Override
     public boolean delete(@NotNull R keyRec) {
-        throw new UnsupportedOperationException("Not implemented yet.");
+        return sync(deleteAsync(keyRec));
     }
-
+    
     /** {@inheritDoc} */
     @Override
     public @NotNull CompletableFuture<Boolean> deleteAsync(@NotNull R keyRec) {
-        throw new UnsupportedOperationException("Not implemented yet.");
+        BinaryRow row = marshalKey(keyRec);
+    
+        return tbl.delete(row, tx);
     }
-
+    
     /** {@inheritDoc} */
     @Override
     public boolean deleteExact(@NotNull R rec) {
-        throw new UnsupportedOperationException("Not implemented yet.");
+        return sync(deleteExactAsync(rec));
     }
-
+    
     /** {@inheritDoc} */
     @Override
     public @NotNull CompletableFuture<Boolean> deleteExactAsync(@NotNull R rec) {
-        throw new UnsupportedOperationException("Not implemented yet.");
+        BinaryRow row = marshal(rec);
+    
+        return tbl.deleteExact(row, tx);
     }
-
+    
     /** {@inheritDoc} */
     @Override
     public R getAndDelete(@NotNull R rec) {
-        throw new UnsupportedOperationException("Not implemented yet.");
+        return sync(getAndDeleteAsync(rec));
     }
-
+    
     /** {@inheritDoc} */
     @Override
-    public @NotNull CompletableFuture<R> getAndDeleteAsync(@NotNull R rec) {
-        throw new UnsupportedOperationException("Not implemented yet.");
+    public @NotNull CompletableFuture<R> getAndDeleteAsync(@NotNull R keyRec) {
+        BinaryRow row = marshalKey(keyRec);
+    
+        return tbl.getAndDelete(row, tx).thenApply(this::unmarshal);
     }
-
+    
     /** {@inheritDoc} */
     @Override
     public Collection<R> deleteAll(@NotNull Collection<R> recs) {
-        throw new UnsupportedOperationException("Not implemented yet.");
+        return sync(deleteAllAsync(recs));
     }
-
+    
     /** {@inheritDoc} */
     @Override
     public @NotNull CompletableFuture<Collection<R>> deleteAllAsync(@NotNull Collection<R> recs) {
         throw new UnsupportedOperationException("Not implemented yet.");
     }
-
+    
     /** {@inheritDoc} */
     @Override
     public Collection<R> deleteAllExact(@NotNull Collection<R> recs) {
-        throw new UnsupportedOperationException("Not implemented yet.");
+        return sync(deleteAllExactAsync(recs));
     }
-
+    
     /** {@inheritDoc} */
     @Override
     public @NotNull CompletableFuture<Collection<R>> deleteAllExactAsync(@NotNull Collection<R> recs) {
         throw new UnsupportedOperationException("Not implemented yet.");
     }
-
+    
     /** {@inheritDoc} */
     @Override
     public <T extends Serializable> T invoke(@NotNull R keyRec, InvokeProcessor<R, R, T> proc) {
         throw new UnsupportedOperationException("Not implemented yet.");
     }
-
+    
     /** {@inheritDoc} */
     @Override
     public @NotNull <T extends Serializable> CompletableFuture<T> invokeAsync(
@@ -252,7 +282,7 @@ public class RecordViewImpl<R> extends AbstractTableView implements RecordView<R
     ) {
         throw new UnsupportedOperationException("Not implemented yet.");
     }
-
+    
     /** {@inheritDoc} */
     @Override
     public <T extends Serializable> Map<R, T> invokeAll(
@@ -261,7 +291,7 @@ public class RecordViewImpl<R> extends AbstractTableView implements RecordView<R
     ) {
         throw new UnsupportedOperationException("Not implemented yet.");
     }
-
+    
     /** {@inheritDoc} */
     @Override
     public @NotNull <T extends Serializable> CompletableFuture<Map<R, T>> invokeAllAsync(
@@ -270,20 +300,83 @@ public class RecordViewImpl<R> extends AbstractTableView implements RecordView<R
     ) {
         throw new UnsupportedOperationException("Not implemented yet.");
     }
-
+    
     /** {@inheritDoc} */
     @Override
     public RecordViewImpl<R> withTransaction(Transaction tx) {
         throw new UnsupportedOperationException("Not implemented yet.");
     }
-
+    
+    /**
     /**
      * Returns marshaller.
+     *
+     * @param schemaVersion Schema version.
+     * @return Marshaller.
      */
-    private RecordSerializer<R> serializer() {
-        throw new UnsupportedOperationException("Not implemented yet.");
+    private RecordMarshaller<R> marshaller(int schemaVersion) {
+        if (marsh == null || marsh.schemaVersion() == schemaVersion) {
+            // TODO: Cache marshaller for schema version or upgrade row?
+            marsh = marshallerFactory.apply(schemaReg.schema(schemaVersion));
+        }
+        
+        return marsh;
     }
-
+    
+    /**
+     * Marshals given record to a row.
+     *
+     * @param rec Record object.
+     * @return Binary row.
+     */
+    private BinaryRow marshal(@NotNull R rec) {
+        final RecordMarshaller<R> marsh = marshaller(schemaReg.lastSchemaVersion());
+        
+        try {
+            return marsh.marshal(rec);
+        } catch (MarshallerException e) {
+            throw new IgniteException(e);
+        }
+    }
+    
+    /**
+     * Marshals given key record to a row.
+     *
+     * @param rec Record key object.
+     * @return Binary row.
+     */
+    private BinaryRow marshalKey(@NotNull R rec) {
+        final RecordMarshaller<R> marsh = marshaller(schemaReg.lastSchemaVersion());
+        
+        try {
+            return marsh.marshalKey(rec);
+        } catch (MarshallerException e) {
+            throw new IgniteException(e);
+        }
+    }
+    
+    /**
+     * Unmarshal value object from given binary row.
+     *
+     * @param binaryRow Binary row.
+     * @return Value object.
+     */
+    private R unmarshal(BinaryRow binaryRow) {
+        if (binaryRow == null || !binaryRow.hasValue()) {
+            return null;
+        }
+        
+        Row row = schemaReg.resolve(binaryRow);
+        
+        RecordMarshaller<R> marshaller = marshaller(row.schemaVersion());
+        
+        try {
+            return marshaller.unmarshal(row);
+        } catch (MarshallerException e) {
+            throw new IgniteException(e);
+        }
+    }
+    
     /**
      * Returns schema-aware row.
      *
@@ -293,9 +386,9 @@ public class RecordViewImpl<R> extends AbstractTableView implements RecordView<R
         if (row == null) {
             return null;
         }
-
+        
         final SchemaDescriptor rowSchema = schemaReg.schema(row.schemaVersion()); // Get a schema for row.
-
+        
         return new Row(rowSchema, row);
     }
 }
diff --git a/modules/table/src/test/java/org/apache/ignite/internal/table/KeyValueViewOperationsTest.java b/modules/table/src/test/java/org/apache/ignite/internal/table/KeyValueViewOperationsTest.java
index 9416953..3f9b361 100644
--- a/modules/table/src/test/java/org/apache/ignite/internal/table/KeyValueViewOperationsTest.java
+++ b/modules/table/src/test/java/org/apache/ignite/internal/table/KeyValueViewOperationsTest.java
@@ -126,11 +126,11 @@ public class KeyValueViewOperationsTest {
         
         assertNull(tbl.get(key));
         
-        // Insert new tuple.
+        // Insert new KV pair.
         assertNull(tbl.getAndPut(key, obj));
-        
         assertEquals(obj, tbl.get(key));
-        
+    
+        // Update KV pair.
         assertEquals(obj, tbl.getAndPut(key, obj2));
         assertEquals(obj2, tbl.getAndPut(key, obj3));
         
diff --git a/modules/table/src/test/java/org/apache/ignite/internal/table/RecordViewOperationsTest.java b/modules/table/src/test/java/org/apache/ignite/internal/table/RecordViewOperationsTest.java
new file mode 100644
index 0000000..602a849
--- /dev/null
+++ b/modules/table/src/test/java/org/apache/ignite/internal/table/RecordViewOperationsTest.java
@@ -0,0 +1,340 @@
+/*
+ * 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.internal.table;
+
+import static org.apache.ignite.internal.schema.NativeTypes.BYTES;
+import static org.apache.ignite.internal.schema.NativeTypes.DATE;
+import static org.apache.ignite.internal.schema.NativeTypes.DOUBLE;
+import static org.apache.ignite.internal.schema.NativeTypes.FLOAT;
+import static org.apache.ignite.internal.schema.NativeTypes.INT16;
+import static org.apache.ignite.internal.schema.NativeTypes.INT32;
+import static org.apache.ignite.internal.schema.NativeTypes.INT64;
+import static org.apache.ignite.internal.schema.NativeTypes.INT8;
+import static org.apache.ignite.internal.schema.NativeTypes.STRING;
+import static org.apache.ignite.internal.schema.NativeTypes.datetime;
+import static org.apache.ignite.internal.schema.NativeTypes.time;
+import static org.apache.ignite.internal.schema.NativeTypes.timestamp;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Random;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.ignite.internal.schema.Column;
+import org.apache.ignite.internal.schema.NativeTypeSpec;
+import org.apache.ignite.internal.schema.NativeTypes;
+import org.apache.ignite.internal.schema.SchemaDescriptor;
+import org.apache.ignite.internal.schema.testobjects.TestObjectWithAllTypes;
+import org.apache.ignite.internal.table.impl.DummyInternalTableImpl;
+import org.apache.ignite.internal.table.impl.DummySchemaManagerImpl;
+import org.apache.ignite.table.RecordView;
+import org.apache.ignite.table.mapper.Mapper;
+import org.jetbrains.annotations.NotNull;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Basic table operations test.
+ */
+//TODO: IGNITE-14487 Add bulk operations tests.
+//TODO: IGNITE-14487 Add async operations tests.
+public class RecordViewOperationsTest {
+    
+    private final Random rnd = new Random();
+    
+    @Test
+    public void upsert() {
+        final TestObjectWithAllTypes key = key(rnd);
+        
+        final TestObjectWithAllTypes obj = randomObject(rnd, key);
+        final TestObjectWithAllTypes obj2 = randomObject(rnd, key);
+        final TestObjectWithAllTypes obj3 = randomObject(rnd, key);
+        
+        RecordView<TestObjectWithAllTypes> tbl = recordView();
+        
+        assertNull(tbl.get(key));
+        
+        // Insert new row.
+        tbl.upsert(obj);
+        assertEquals(obj, tbl.get(key));
+        
+        // Upsert row.
+        tbl.upsert(obj2);
+        assertEquals(obj2, tbl.get(key));
+        
+        // Remove row.
+        tbl.delete(key);
+        assertNull(tbl.get(key));
+        
+        // Insert new row.
+        tbl.upsert(obj3);
+        assertEquals(obj3, tbl.get(key));
+    }
+    
+    @Test
+    public void insert() {
+        final TestObjectWithAllTypes key = key(rnd);
+        final TestObjectWithAllTypes obj = randomObject(rnd, key);
+        final TestObjectWithAllTypes obj2 = randomObject(rnd, key);
+        
+        RecordView<TestObjectWithAllTypes> tbl = recordView();
+        
+        assertNull(tbl.get(key));
+        
+        // Insert new row.
+        assertTrue(tbl.insert(obj));
+        assertEquals(obj, tbl.get(key));
+        
+        // Ignore existed row pair.
+        assertFalse(tbl.insert(obj2));
+        assertEquals(obj, tbl.get(key));
+    }
+    
+    @Test
+    public void getAndUpsert() {
+        final TestObjectWithAllTypes key = key(rnd);
+        final TestObjectWithAllTypes obj = randomObject(rnd, key);
+        final TestObjectWithAllTypes obj2 = randomObject(rnd, key);
+        final TestObjectWithAllTypes obj3 = randomObject(rnd, key);
+        
+        RecordView<TestObjectWithAllTypes> tbl = recordView();
+        
+        assertNull(tbl.get(key));
+        
+        // Insert new row.
+        assertNull(tbl.getAndUpsert(obj));
+        assertEquals(obj, tbl.get(key));
+        
+        // Update exited row.
+        assertEquals(obj, tbl.getAndUpsert(obj2));
+        assertEquals(obj2, tbl.getAndUpsert(obj3));
+        
+        assertEquals(obj3, tbl.get(key));
+    }
+    
+    @Test
+    public void remove() {
+        final TestObjectWithAllTypes key = key(rnd);
+        final TestObjectWithAllTypes obj = randomObject(rnd, key);
+        final TestObjectWithAllTypes obj2 = randomObject(rnd, key);
+        
+        RecordView<TestObjectWithAllTypes> tbl = recordView();
+        
+        // Delete not existed key.
+        assertNull(tbl.get(key));
+        assertFalse(tbl.delete(key));
+        
+        // Insert a new row.
+        tbl.upsert(obj);
+        
+        // Delete existed row.
+        assertEquals(obj, tbl.get(key));
+        assertTrue(tbl.delete(key));
+        assertNull(tbl.get(key));
+        
+        // Delete already deleted row.
+        assertFalse(tbl.delete(key));
+        
+        // Insert a new row.
+        tbl.upsert(obj2);
+        assertEquals(obj2, tbl.get(key));
+    }
+    
+    @Test
+    public void removeExact() {
+        final TestObjectWithAllTypes key = key(rnd);
+        final TestObjectWithAllTypes obj = randomObject(rnd, key);
+        final TestObjectWithAllTypes obj2 = randomObject(rnd, key);
+        
+        RecordView<TestObjectWithAllTypes> tbl = recordView();
+        
+        // Insert a new row.
+        tbl.upsert(obj);
+        assertEquals(obj, tbl.get(key));
+        
+        // Fails to delete row with unexpected value.
+        assertFalse(tbl.deleteExact(obj2));
+        assertEquals(obj, tbl.get(key));
+        
+        // Delete row with expected value.
+        assertTrue(tbl.deleteExact(obj));
+        assertNull(tbl.get(key));
+        
+        // Try to remove non-existed key.
+        assertFalse(tbl.deleteExact(obj));
+        assertNull(tbl.get(key));
+        
+        // Insert a new row.
+        tbl.upsert(obj2);
+        assertEquals(obj2, tbl.get(key));
+        
+        // Delete row with expected value.
+        assertTrue(tbl.delete(obj2));
+        assertNull(tbl.get(key));
+        
+        assertFalse(tbl.delete(obj2));
+        assertNull(tbl.get(obj2));
+    }
+    
+    @Test
+    public void replace() {
+        final TestObjectWithAllTypes key = key(rnd);
+        final TestObjectWithAllTypes obj = randomObject(rnd, key);
+        final TestObjectWithAllTypes obj2 = randomObject(rnd, key);
+        final TestObjectWithAllTypes obj3 = randomObject(rnd, key);
+        
+        RecordView<TestObjectWithAllTypes> tbl = recordView();
+        
+        // Ignore replace operation for non-existed row.
+        assertFalse(tbl.replace(obj));
+        assertNull(tbl.get(key));
+        
+        // Insert new row.
+        tbl.upsert(obj);
+        
+        // Replace existed row.
+        assertTrue(tbl.replace(obj2));
+        assertEquals(obj2, tbl.get(key));
+        
+        // Replace existed row.
+        assertTrue(tbl.replace(obj3));
+        assertEquals(obj3, tbl.get(key));
+        
+        // Remove existed row.
+        assertTrue(tbl.delete(key));
+        assertNull(tbl.get(key));
+        
+        tbl.upsert(obj);
+        assertEquals(obj, tbl.get(key));
+    }
+    
+    @Test
+    public void replaceExact() {
+        final TestObjectWithAllTypes key = key(rnd);
+        final TestObjectWithAllTypes obj = randomObject(rnd, key);
+        final TestObjectWithAllTypes obj2 = randomObject(rnd, key);
+        final TestObjectWithAllTypes obj3 = randomObject(rnd, key);
+        final TestObjectWithAllTypes obj4 = randomObject(rnd, key);
+        
+        RecordView<TestObjectWithAllTypes> tbl = recordView();
+        
+        // Ignore replace operation for non-existed row.
+        assertFalse(tbl.replace(obj, obj2));
+        assertNull(tbl.get(key));
+        
+        // Insert new row.
+        tbl.upsert(obj);
+        
+        // Ignore un-exepected row replacement.
+        assertFalse(tbl.replace(obj2, obj3));
+        assertEquals(obj, tbl.get(key));
+        
+        // Replace existed row.
+        assertTrue(tbl.replace(obj, obj2));
+        assertEquals(obj2, tbl.get(key));
+        
+        // Replace existed KV pair.
+        assertTrue(tbl.replace(obj2, obj3));
+        assertEquals(obj3, tbl.get(key));
+        
+        // Remove existed row.
+        assertTrue(tbl.delete(key));
+        assertNull(tbl.get(key));
+        
+        assertFalse(tbl.replace(key, obj4));
+        assertNull(tbl.get(key));
+    }
+    
+    /**
+     * Creates RecordView.
+     */
+    private RecordViewImpl<TestObjectWithAllTypes> recordView() {
+        Mapper<TestObjectWithAllTypes> recMapper = Mapper.identity(TestObjectWithAllTypes.class);
+        
+        Column[] valCols = {
+                new Column("primitiveByteCol", INT8, false),
+                new Column("primitiveShortCol", INT16, false),
+                new Column("primitiveIntCol", INT32, false),
+                new Column("primitiveFloatCol", FLOAT, false),
+                new Column("primitiveDoubleCol", DOUBLE, false),
+                
+                new Column("byteCol", INT8, true),
+                new Column("shortCol", INT16, true),
+                new Column("intCol", INT32, true),
+                new Column("longCol", INT64, true),
+                new Column("nullLongCol", INT64, true),
+                new Column("floatCol", FLOAT, true),
+                new Column("doubleCol", DOUBLE, true),
+                
+                new Column("dateCol", DATE, true),
+                new Column("timeCol", time(), true),
+                new Column("dateTimeCol", datetime(), true),
+                new Column("timestampCol", timestamp(), true),
+                
+                new Column("uuidCol", NativeTypes.UUID, true),
+                new Column("bitmaskCol", NativeTypes.bitmaskOf(42), true),
+                new Column("stringCol", STRING, true),
+                new Column("nullBytesCol", BYTES, true),
+                new Column("bytesCol", BYTES, true),
+                new Column("numberCol", NativeTypes.numberOf(12), true),
+                new Column("decimalCol", NativeTypes.decimalOf(19, 3), true),
+        };
+        
+        SchemaDescriptor schema = new SchemaDescriptor(
+                1,
+                new Column[]{new Column("primitiveLongCol", NativeTypes.INT64, false)},
+                valCols
+        );
+        
+        // Validate all types are tested.
+        Set<NativeTypeSpec> testedTypes = Arrays.stream(valCols).map(c -> c.type().spec())
+                .collect(Collectors.toSet());
+        Set<NativeTypeSpec> missedTypes = Arrays.stream(NativeTypeSpec.values())
+                .filter(t -> !testedTypes.contains(t)).collect(Collectors.toSet());
+        
+        assertEquals(Collections.emptySet(), missedTypes);
+        
+        return new RecordViewImpl<>(
+                new DummyInternalTableImpl(),
+                new DummySchemaManagerImpl(schema),
+                recMapper,
+                null
+        );
+    }
+    
+    @NotNull
+    private TestObjectWithAllTypes randomObject(Random rnd, TestObjectWithAllTypes key) {
+        TestObjectWithAllTypes obj = TestObjectWithAllTypes.randomObject(rnd);
+        
+        obj.setPrimitiveLongCol(key.getPrimitiveLongCol());
+        
+        return obj;
+    }
+    
+    @NotNull
+    private static TestObjectWithAllTypes key(Random rnd) {
+        TestObjectWithAllTypes key = new TestObjectWithAllTypes();
+        
+        key.setPrimitiveLongCol(rnd.nextLong());
+        
+        return key;
+    }
+}