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 c7d44a5  IGNITE-15519: Make tuples serializable (#343)
c7d44a5 is described below

commit c7d44a54548f2225962261e2a5d8c535bfdf5a25
Author: Andrew V. Mashenkov <[email protected]>
AuthorDate: Fri Sep 17 14:21:08 2021 +0300

    IGNITE-15519: Make tuples serializable (#343)
---
 .../java/org/apache/ignite/table/TupleImpl.java    |  55 ++++---
 .../handler/requests/table/ClientTableCommon.java  |   6 +-
 .../java/org/apache/ignite/client/CustomTuple.java |  28 ++--
 .../schema/registry/UpgradingRowAdapter.java       |   3 +-
 .../org/apache/ignite/internal/schema/row/Row.java |   8 +-
 .../internal/table/AbstractRowTupleAdapter.java    |  41 ++---
 .../internal/table/MutableRowTupleAdapter.java     |  88 ++++++++--
 .../internal/table/MutableRowTupleAdapterTest.java | 179 ++++++++++++++++++++-
 .../internal/table/impl/TestTupleBuilder.java      |  28 ++--
 .../org/apache/ignite/table/TupleImplTest.java     |  62 ++++++-
 10 files changed, 396 insertions(+), 102 deletions(-)

diff --git a/modules/api/src/main/java/org/apache/ignite/table/TupleImpl.java 
b/modules/api/src/main/java/org/apache/ignite/table/TupleImpl.java
index 279dd27..d6481cf 100644
--- a/modules/api/src/main/java/org/apache/ignite/table/TupleImpl.java
+++ b/modules/api/src/main/java/org/apache/ignite/table/TupleImpl.java
@@ -17,6 +17,8 @@
 
 package org.apache.ignite.table;
 
+import java.io.IOException;
+import java.io.Serializable;
 import java.time.Instant;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
@@ -35,9 +37,16 @@ import org.jetbrains.annotations.NotNull;
 /**
  * Simple tuple implementation.
  */
-public class TupleImpl implements Tuple {
-    /** Column name -&gt; index mapping. */
-    private final Map<String, Integer> colIdxMap;
+public class TupleImpl implements Tuple, Serializable {
+    /** Version UID. */
+    private static final long serialVersionUID = 0L;
+
+    /**
+     * Column name -&gt; index mapping.
+     * <p>
+     * Note: Transient because it's recoverable from {@link #colNames}.
+     */
+    private transient Map<String, Integer> colIdxMap;
 
     /** Columns names. */
     private final List<String> colNames;
@@ -49,7 +58,7 @@ public class TupleImpl implements Tuple {
      * Creates tuple.
      */
     public TupleImpl() {
-        this(new HashMap<>(), new ArrayList(), new ArrayList());
+        this(new HashMap<>(), new ArrayList<>(), new ArrayList<>());
     }
 
     /**
@@ -58,16 +67,7 @@ public class TupleImpl implements Tuple {
      * @param capacity Initial capacity.
      */
     public TupleImpl(int capacity) {
-        this(new HashMap<>(capacity), new ArrayList(capacity), new 
ArrayList(capacity));
-    }
-
-    /**
-     * Copying constructor.
-     *
-     * @param tuple Tuple to copy.
-     */
-    public TupleImpl(@NotNull TupleImpl tuple) {
-        this(new HashMap<>(tuple.colIdxMap), new ArrayList<>(tuple.colNames), 
new ArrayList<>(tuple.vals));
+        this(new HashMap<>(capacity), new ArrayList<>(capacity), new 
ArrayList<>(capacity));
     }
 
     /**
@@ -97,9 +97,7 @@ public class TupleImpl implements Tuple {
 
     /** {@inheritDoc} */
     @Override public Tuple set(@NotNull String columnName, Object val) {
-        Objects.nonNull(columnName);
-
-        int idx = colIdxMap.computeIfAbsent(columnName, name -> 
colIdxMap.size());
+        int idx = 
colIdxMap.computeIfAbsent(Objects.requireNonNull(columnName), name -> 
colIdxMap.size());
 
         if (idx == colNames.size()) {
             colNames.add(idx, columnName);
@@ -137,7 +135,7 @@ public class TupleImpl implements Tuple {
     @Override public <T> T valueOrDefault(@NotNull String columnName, T def) {
         int idx = columnIndex(columnName);
 
-        return (idx == -1) ? def : (T) vals.get(idx);
+        return (idx == -1) ? def : (T)vals.get(idx);
     }
 
     /** {@inheritDoc} */
@@ -147,14 +145,14 @@ public class TupleImpl implements Tuple {
         if (idx == -1)
             throw new IllegalArgumentException("Column not found: columnName=" 
+ columnName);
 
-        return (T) vals.get(idx);
+        return (T)vals.get(idx);
     }
 
     /** {@inheritDoc} */
     @Override public <T> T value(int columnIndex) {
         Objects.checkIndex(columnIndex, vals.size());
 
-        return (T) vals.get(columnIndex);
+        return (T)vals.get(columnIndex);
     }
 
     /** {@inheritDoc} */
@@ -314,4 +312,21 @@ public class TupleImpl implements Tuple {
             }
         };
     }
+
+    /**
+     * Deserializes object.
+     *
+     * @param in Input object stream.
+     * @throws IOException            If failed.
+     * @throws ClassNotFoundException If failed.
+     */
+    private void readObject(java.io.ObjectInputStream in) throws IOException, 
ClassNotFoundException {
+        in.defaultReadObject();
+
+        // Recover column name->index mapping.
+        colIdxMap = new HashMap<>(colNames.size());
+
+        for (int i = 0; i < colNames.size(); i++)
+            colIdxMap.put(colNames.get(i), i);
+    }
 }
diff --git 
a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/table/ClientTableCommon.java
 
b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/table/ClientTableCommon.java
index eca5f0f..ac5580a 100644
--- 
a/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/table/ClientTableCommon.java
+++ 
b/modules/client-handler/src/main/java/org/apache/ignite/client/handler/requests/table/ClientTableCommon.java
@@ -287,10 +287,10 @@ class ClientTableCommon {
             boolean keyOnly,
             SchemaDescriptor schema
     ) {
-        var tuple = Tuple.create();
-
         var cnt = keyOnly ? schema.keyColumns().length() : schema.length();
 
+        var tuple = Tuple.create(cnt);
+
         for (int i = 0; i < cnt; i++) {
             if (unpacker.getNextFormat() == MessageFormat.NIL) {
                 unpacker.skipValue();
@@ -311,7 +311,7 @@ class ClientTableCommon {
      */
     public static Tuple readTupleSchemaless(ClientMessageUnpacker unpacker) {
         var cnt = unpacker.unpackMapHeader();
-        var tuple = Tuple.create();
+        var tuple = Tuple.create(cnt);
 
         for (int i = 0; i < cnt; i++) {
             var colName = unpacker.unpackString();
diff --git 
a/modules/client/src/test/java/org/apache/ignite/client/CustomTuple.java 
b/modules/client/src/test/java/org/apache/ignite/client/CustomTuple.java
index 55bdf4e..8d3db6e 100644
--- a/modules/client/src/test/java/org/apache/ignite/client/CustomTuple.java
+++ b/modules/client/src/test/java/org/apache/ignite/client/CustomTuple.java
@@ -64,7 +64,7 @@ public class CustomTuple implements Tuple {
         return null;
     }
 
-    @Override public int columnIndex(String columnName) {
+    @Override public int columnIndex(@NotNull String columnName) {
         switch (columnName) {
             case "id":
                 return 0;
@@ -75,7 +75,7 @@ public class CustomTuple implements Tuple {
         return -1;
     }
 
-    @Override public <T> T valueOrDefault(String columnName, T def) {
+    @Override public <T> T valueOrDefault(@NotNull String columnName, T def) {
         switch (columnName) {
             case "id":
                 return (T)id;
@@ -86,11 +86,11 @@ public class CustomTuple implements Tuple {
         return def;
     }
 
-    @Override public Tuple set(String columnName, Object value) {
+    @Override public Tuple set(@NotNull String columnName, Object value) {
         throw new UnsupportedOperationException("Tuple is immutable.");
     }
 
-    @Override public <T> T value(String columnName) {
+    @Override public <T> T value(@NotNull String columnName) {
         return valueOrDefault(columnName, null);
     }
 
@@ -105,7 +105,7 @@ public class CustomTuple implements Tuple {
         return null;
     }
 
-    @Override public BinaryObject binaryObjectValue(String columnName) {
+    @Override public BinaryObject binaryObjectValue(@NotNull String 
columnName) {
         throw new UnsupportedOperationException();
     }
 
@@ -113,7 +113,7 @@ public class CustomTuple implements Tuple {
         throw new UnsupportedOperationException();
     }
 
-    @Override public byte byteValue(String columnName) {
+    @Override public byte byteValue(@NotNull String columnName) {
         throw new UnsupportedOperationException();
     }
 
@@ -121,7 +121,7 @@ public class CustomTuple implements Tuple {
         throw new UnsupportedOperationException();
     }
 
-    @Override public short shortValue(String columnName) {
+    @Override public short shortValue(@NotNull String columnName) {
         throw new UnsupportedOperationException();
     }
 
@@ -129,7 +129,7 @@ public class CustomTuple implements Tuple {
         throw new UnsupportedOperationException();
     }
 
-    @Override public int intValue(String columnName) {
+    @Override public int intValue(@NotNull String columnName) {
         throw new UnsupportedOperationException();
     }
 
@@ -137,7 +137,7 @@ public class CustomTuple implements Tuple {
         throw new UnsupportedOperationException();
     }
 
-    @Override public long longValue(String columnName) {
+    @Override public long longValue(@NotNull String columnName) {
         throw new UnsupportedOperationException();
     }
 
@@ -145,7 +145,7 @@ public class CustomTuple implements Tuple {
         throw new UnsupportedOperationException();
     }
 
-    @Override public float floatValue(String columnName) {
+    @Override public float floatValue(@NotNull String columnName) {
         throw new UnsupportedOperationException();
     }
 
@@ -153,7 +153,7 @@ public class CustomTuple implements Tuple {
         throw new UnsupportedOperationException();
     }
 
-    @Override public double doubleValue(String columnName) {
+    @Override public double doubleValue(@NotNull String columnName) {
         throw new UnsupportedOperationException();
     }
 
@@ -161,7 +161,7 @@ public class CustomTuple implements Tuple {
         throw new UnsupportedOperationException();
     }
 
-    @Override public String stringValue(String columnName) {
+    @Override public String stringValue(@NotNull String columnName) {
         throw new UnsupportedOperationException();
     }
 
@@ -169,7 +169,7 @@ public class CustomTuple implements Tuple {
         throw new UnsupportedOperationException();
     }
 
-    @Override public UUID uuidValue(String columnName) {
+    @Override public UUID uuidValue(@NotNull String columnName) {
         throw new UnsupportedOperationException();
     }
 
@@ -177,7 +177,7 @@ public class CustomTuple implements Tuple {
         throw new UnsupportedOperationException();
     }
 
-    @Override public BitSet bitmaskValue(String columnName) {
+    @Override public BitSet bitmaskValue(@NotNull String columnName) {
         throw new UnsupportedOperationException();
     }
 
diff --git 
a/modules/schema/src/main/java/org/apache/ignite/internal/schema/registry/UpgradingRowAdapter.java
 
b/modules/schema/src/main/java/org/apache/ignite/internal/schema/registry/UpgradingRowAdapter.java
index f990e5c..9da0a54 100644
--- 
a/modules/schema/src/main/java/org/apache/ignite/internal/schema/registry/UpgradingRowAdapter.java
+++ 
b/modules/schema/src/main/java/org/apache/ignite/internal/schema/registry/UpgradingRowAdapter.java
@@ -33,6 +33,7 @@ import org.apache.ignite.internal.schema.SchemaDescriptor;
 import org.apache.ignite.internal.schema.SchemaException;
 import org.apache.ignite.internal.schema.mapping.ColumnMapper;
 import org.apache.ignite.internal.schema.row.Row;
+import org.jetbrains.annotations.NotNull;
 
 /**
  * Adapter for row of older schema.
@@ -58,7 +59,7 @@ class UpgradingRowAdapter extends Row {
     }
 
     /** {@inheritDoc} */
-    @Override public SchemaDescriptor schema() {
+    @Override @NotNull public SchemaDescriptor schema() {
         return schema;
     }
 
diff --git 
a/modules/schema/src/main/java/org/apache/ignite/internal/schema/row/Row.java 
b/modules/schema/src/main/java/org/apache/ignite/internal/schema/row/Row.java
index 9d141a7..91fb5be 100644
--- 
a/modules/schema/src/main/java/org/apache/ignite/internal/schema/row/Row.java
+++ 
b/modules/schema/src/main/java/org/apache/ignite/internal/schema/row/Row.java
@@ -37,6 +37,7 @@ import org.apache.ignite.internal.schema.NativeTypeSpec;
 import org.apache.ignite.internal.schema.SchemaAware;
 import org.apache.ignite.internal.schema.SchemaDescriptor;
 import org.apache.ignite.internal.schema.TemporalNativeType;
+import org.jetbrains.annotations.NotNull;
 
 /**
  * Schema-aware row.
@@ -71,7 +72,7 @@ public class Row implements BinaryRow, SchemaAware {
     /**
      * @return Row schema.
      */
-    @Override public SchemaDescriptor schema() {
+    @Override @NotNull public SchemaDescriptor schema() {
         return schema;
     }
 
@@ -486,13 +487,10 @@ public class Row implements BinaryRow, SchemaAware {
      *
      * @param colIdx Column index.
      * @param type Expected column type.
-     * @return {@code -1} if value is {@code null} for a column,
-     * or {@link Long#MAX_VALUE} if column is unknown,
-     * otherwise encoded offset + length of the column.
+     * @return {@code -1} if value is {@code null} for a column, otherwise 
encoded offset + length of the column.
      * @see #offset(long)
      * @see #length(long)
      * @see InvalidTypeException If actual column type does not match the 
requested column type.
-     * @see org.apache.ignite.internal.schema.registry.UpgradingRowAdapter
      */
     protected long findColumn(int colIdx, NativeTypeSpec type) throws 
InvalidTypeException {
         // Get base offset (key start or value start) for the given column.
diff --git 
a/modules/table/src/main/java/org/apache/ignite/internal/table/AbstractRowTupleAdapter.java
 
b/modules/table/src/main/java/org/apache/ignite/internal/table/AbstractRowTupleAdapter.java
index 51891ec..f1062f9 100644
--- 
a/modules/table/src/main/java/org/apache/ignite/internal/table/AbstractRowTupleAdapter.java
+++ 
b/modules/table/src/main/java/org/apache/ignite/internal/table/AbstractRowTupleAdapter.java
@@ -33,14 +33,16 @@ import org.apache.ignite.internal.schema.SchemaDescriptor;
 import org.apache.ignite.internal.schema.row.Row;
 import org.apache.ignite.table.Tuple;
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 /**
  * Abstract row tuple adapter.
  */
 public abstract class AbstractRowTupleAdapter implements Tuple, SchemaAware {
-    /** Underlying row. */
-    protected Row row;
+    /**
+     * Underlying row.
+     * Note: Marked transient to prevent unwanted serialization of the schema 
and\or other context.
+     */
+    protected transient Row row;
 
     /**
      * Creates Tuple adapter for row.
@@ -52,8 +54,8 @@ public abstract class AbstractRowTupleAdapter implements 
Tuple, SchemaAware {
     }
 
     /** {@inheritDoc} */
-    @Override @Nullable public SchemaDescriptor schema() {
-        return row == null ? null : row.schema();
+    @Override public SchemaDescriptor schema() {
+        return row.schema();
     }
 
     /** {@inheritDoc} */
@@ -81,25 +83,25 @@ public abstract class AbstractRowTupleAdapter implements 
Tuple, SchemaAware {
 
         final Column col = row.schema().column(columnName);
 
-        return col == null ? defaultValue : (T) 
col.type().spec().objectValue(row, col.schemaIndex());
+        return col == null ? defaultValue : 
(T)col.type().spec().objectValue(row, col.schemaIndex());
     }
 
     /** {@inheritDoc} */
     @Override public <T> T value(@NotNull String columnName) {
         final Column col = rowColumnByName(columnName);
 
-        return (T) col.type().spec().objectValue(row, col.schemaIndex());
+        return (T)col.type().spec().objectValue(row, col.schemaIndex());
     }
 
     @Override public <T> T value(int columnIndex) {
         Column col = rowColumnByIndex(columnIndex);
 
-        return (T) col.type().spec().objectValue(row, col.schemaIndex());
+        return (T)col.type().spec().objectValue(row, col.schemaIndex());
     }
 
 
     /** {@inheritDoc} */
-    @Override public BinaryObject binaryObjectValue(String columnName) {
+    @Override public BinaryObject binaryObjectValue(@NotNull String 
columnName) {
         Column col = rowColumnByName(columnName);
 
         return BinaryObjects.wrap(row.bytesValue(col.schemaIndex()));
@@ -113,7 +115,7 @@ public abstract class AbstractRowTupleAdapter implements 
Tuple, SchemaAware {
     }
 
     /** {@inheritDoc} */
-    @Override public byte byteValue(String columnName) {
+    @Override public byte byteValue(@NotNull String columnName) {
         Column col = rowColumnByName(columnName);
 
         return row.byteValue(col.schemaIndex());
@@ -127,7 +129,7 @@ public abstract class AbstractRowTupleAdapter implements 
Tuple, SchemaAware {
     }
 
     /** {@inheritDoc} */
-    @Override public short shortValue(String columnName) {
+    @Override public short shortValue(@NotNull String columnName) {
         Column col = rowColumnByName(columnName);
 
         return row.shortValue(col.schemaIndex());
@@ -141,7 +143,7 @@ public abstract class AbstractRowTupleAdapter implements 
Tuple, SchemaAware {
     }
 
     /** {@inheritDoc} */
-    @Override public int intValue(String columnName) {
+    @Override public int intValue(@NotNull String columnName) {
         Column col = rowColumnByName(columnName);
 
         return row.intValue(col.schemaIndex());
@@ -155,7 +157,7 @@ public abstract class AbstractRowTupleAdapter implements 
Tuple, SchemaAware {
     }
 
     /** {@inheritDoc} */
-    @Override public long longValue(String columnName) {
+    @Override public long longValue(@NotNull String columnName) {
         Column col = rowColumnByName(columnName);
 
         return row.longValue(col.schemaIndex());
@@ -169,7 +171,7 @@ public abstract class AbstractRowTupleAdapter implements 
Tuple, SchemaAware {
     }
 
     /** {@inheritDoc} */
-    @Override public float floatValue(String columnName) {
+    @Override public float floatValue(@NotNull String columnName) {
         Column col = rowColumnByName(columnName);
 
         return row.floatValue(col.schemaIndex());
@@ -183,7 +185,7 @@ public abstract class AbstractRowTupleAdapter implements 
Tuple, SchemaAware {
     }
 
     /** {@inheritDoc} */
-    @Override public double doubleValue(String columnName) {
+    @Override public double doubleValue(@NotNull String columnName) {
         Column col = rowColumnByName(columnName);
 
         return row.doubleValue(col.schemaIndex());
@@ -197,7 +199,7 @@ public abstract class AbstractRowTupleAdapter implements 
Tuple, SchemaAware {
     }
 
     /** {@inheritDoc} */
-    @Override public String stringValue(String columnName) {
+    @Override public String stringValue(@NotNull String columnName) {
         Column col = rowColumnByName(columnName);
 
         return row.stringValue(col.schemaIndex());
@@ -211,7 +213,7 @@ public abstract class AbstractRowTupleAdapter implements 
Tuple, SchemaAware {
     }
 
     /** {@inheritDoc} */
-    @Override public UUID uuidValue(String columnName) {
+    @Override public UUID uuidValue(@NotNull String columnName) {
         Column col = rowColumnByName(columnName);
 
         return row.uuidValue(col.schemaIndex());
@@ -225,7 +227,7 @@ public abstract class AbstractRowTupleAdapter implements 
Tuple, SchemaAware {
     }
 
     /** {@inheritDoc} */
-    @Override public BitSet bitmaskValue(String columnName) {
+    @Override public BitSet bitmaskValue(@NotNull String columnName) {
         Column col = rowColumnByName(columnName);
 
         return row.bitmaskValue(col.schemaIndex());
@@ -330,7 +332,8 @@ public abstract class AbstractRowTupleAdapter implements 
Tuple, SchemaAware {
     }
 
     /**
-     * Returns row column for index.
+     * Returns row column for given column index.
+     *
      * @param columnIndex Column index.
      * @return Column.
      */
diff --git 
a/modules/table/src/main/java/org/apache/ignite/internal/table/MutableRowTupleAdapter.java
 
b/modules/table/src/main/java/org/apache/ignite/internal/table/MutableRowTupleAdapter.java
index 6c46ca5..12a0aea 100644
--- 
a/modules/table/src/main/java/org/apache/ignite/internal/table/MutableRowTupleAdapter.java
+++ 
b/modules/table/src/main/java/org/apache/ignite/internal/table/MutableRowTupleAdapter.java
@@ -17,6 +17,8 @@
 
 package org.apache.ignite.internal.table;
 
+import java.io.ObjectStreamException;
+import java.io.Serializable;
 import java.time.Instant;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
@@ -25,15 +27,42 @@ import java.util.BitSet;
 import java.util.Iterator;
 import java.util.UUID;
 import org.apache.ignite.binary.BinaryObject;
+import org.apache.ignite.internal.schema.SchemaDescriptor;
 import org.apache.ignite.internal.schema.row.Row;
 import org.apache.ignite.table.Tuple;
 import org.apache.ignite.table.TupleImpl;
 import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 
 /**
- * Mutable tuple adapter for a row.
+ * Adapter provides Tuple access methods and mutability for a Row.
+ * <p>
+ * Once adapter get created, it delegates all data access methods to the 
{@link #row}.
+ * <p>
+ * Mutability.
+ * Underlying Row is immutable serialized object and can't be mutated directly.
+ * Because of this fact, first call of {@link #set(String, Object)} method 
implies the Row conversion to a Tuple.
+ * <p>
+ * After the first mutation the {@link #tuple} becomes a full copy of {@link 
#row},
+ * all data access methods delegate to a {@link #tuple}, and the {@link #row} 
one is no longer useful.
+ * The adapter acts as simple schema-less tuple {@link TupleImpl} and {@link 
#schema()} return null.
+ * <p>
+ * Serialization.
+ * Row access methods implicitly require a context (schema) for a binary data 
reading, The context may be huge
+ * comparing to a row data, and its serialization is unwanted.
+ * So, Row firstly is converted to Tuple.
+ * <p>
+ * Because of after that the adapter will act as underlying tuple {@link 
TupleImpl},
+ * the adapter will be substituted unconditionally with the tuple itself 
during deserialization.
+ *
+ * @see TupleImpl
+ * @see #unmarshalRow()
+ * @see #writeReplace()
  */
-public class MutableRowTupleAdapter extends AbstractRowTupleAdapter {
+public class MutableRowTupleAdapter extends AbstractRowTupleAdapter implements 
Serializable {
+    // Default constructor and serialVersionUID not needed because, actually,
+    // this object never get serialized, it's unconditionally substituted 
during serialization.
+
     /** Tuple with overwritten data. */
     protected TupleImpl tuple;
 
@@ -47,6 +76,11 @@ public class MutableRowTupleAdapter extends 
AbstractRowTupleAdapter {
     }
 
     /** {@inheritDoc} */
+    @Override public @Nullable SchemaDescriptor schema() {
+        return tuple != null ? null : super.schema();
+    }
+
+    /** {@inheritDoc} */
     @Override public int columnCount() {
         return tuple != null ? tuple.columnCount() : super.columnCount();
     }
@@ -77,7 +111,7 @@ public class MutableRowTupleAdapter extends 
AbstractRowTupleAdapter {
     }
 
     /** {@inheritDoc} */
-    @Override public BinaryObject binaryObjectValue(String columnName) {
+    @Override public BinaryObject binaryObjectValue(@NotNull String 
columnName) {
         return tuple != null ? tuple.binaryObjectValue(columnName) : 
super.binaryObjectValue(columnName);
     }
 
@@ -87,7 +121,7 @@ public class MutableRowTupleAdapter extends 
AbstractRowTupleAdapter {
     }
 
     /** {@inheritDoc} */
-    @Override public byte byteValue(String columnName) {
+    @Override public byte byteValue(@NotNull String columnName) {
         return tuple != null ? tuple.byteValue(columnName) : 
super.byteValue(columnName);
     }
 
@@ -97,7 +131,7 @@ public class MutableRowTupleAdapter extends 
AbstractRowTupleAdapter {
     }
 
     /** {@inheritDoc} */
-    @Override public short shortValue(String columnName) {
+    @Override public short shortValue(@NotNull String columnName) {
         return tuple != null ? tuple.shortValue(columnName) : 
super.shortValue(columnName);
     }
 
@@ -107,7 +141,7 @@ public class MutableRowTupleAdapter extends 
AbstractRowTupleAdapter {
     }
 
     /** {@inheritDoc} */
-    @Override public int intValue(String columnName) {
+    @Override public int intValue(@NotNull String columnName) {
         return tuple != null ? tuple.intValue(columnName) : 
super.intValue(columnName);
     }
 
@@ -117,7 +151,7 @@ public class MutableRowTupleAdapter extends 
AbstractRowTupleAdapter {
     }
 
     /** {@inheritDoc} */
-    @Override public long longValue(String columnName) {
+    @Override public long longValue(@NotNull String columnName) {
         return tuple != null ? tuple.longValue(columnName) : 
super.longValue(columnName);
     }
 
@@ -127,7 +161,7 @@ public class MutableRowTupleAdapter extends 
AbstractRowTupleAdapter {
     }
 
     /** {@inheritDoc} */
-    @Override public float floatValue(String columnName) {
+    @Override public float floatValue(@NotNull String columnName) {
         return tuple != null ? tuple.floatValue(columnName) : 
super.floatValue(columnName);
     }
 
@@ -137,7 +171,7 @@ public class MutableRowTupleAdapter extends 
AbstractRowTupleAdapter {
     }
 
     /** {@inheritDoc} */
-    @Override public double doubleValue(String columnName) {
+    @Override public double doubleValue(@NotNull String columnName) {
         return tuple != null ? tuple.doubleValue(columnName) : 
super.doubleValue(columnName);
     }
 
@@ -147,7 +181,7 @@ public class MutableRowTupleAdapter extends 
AbstractRowTupleAdapter {
     }
 
     /** {@inheritDoc} */
-    @Override public String stringValue(String columnName) {
+    @Override public String stringValue(@NotNull String columnName) {
         return tuple != null ? tuple.stringValue(columnName) : 
super.stringValue(columnName);
     }
 
@@ -157,7 +191,7 @@ public class MutableRowTupleAdapter extends 
AbstractRowTupleAdapter {
     }
 
     /** {@inheritDoc} */
-    @Override public UUID uuidValue(String columnName) {
+    @Override public UUID uuidValue(@NotNull String columnName) {
         return tuple != null ? tuple.uuidValue(columnName) : 
super.uuidValue(columnName);
     }
 
@@ -167,7 +201,7 @@ public class MutableRowTupleAdapter extends 
AbstractRowTupleAdapter {
     }
 
     /** {@inheritDoc} */
-    @Override public BitSet bitmaskValue(String columnName) {
+    @Override public BitSet bitmaskValue(@NotNull String columnName) {
         return tuple != null ? tuple.bitmaskValue(columnName) : 
super.bitmaskValue(columnName);
     }
 
@@ -223,15 +257,37 @@ public class MutableRowTupleAdapter extends 
AbstractRowTupleAdapter {
 
     /** {@inheritDoc} */
     @Override public Tuple set(@NotNull String columnName, Object value) {
+        unmarshalRow();
+
+        tuple.set(columnName, value);
+
+        return this;
+    }
+
+    /**
+     * Converts immutable row to mutable tuple.
+     */
+    private void unmarshalRow() {
         if (tuple == null) {
-            TupleImpl tuple0 = new TupleImpl(this);
+            tuple = new TupleImpl(this);
 
-            tuple = tuple0;
             row = null;
         }
+    }
 
-        tuple.set(columnName, value);
+    /**
+     * Overrides default serialization flow.
+     * Serialized {@code tuple} instead of {@code this}, as wrapper make no 
sense after tuple is fully materialized.
+     * <p>
+     * Note: The method has protected modifier and subclass access to this 
method follows java accessibility rules.
+     * Thus, class successors may obtain similar behaviour.
+     *
+     * @return Tuple to serialize.
+     * @throws ObjectStreamException If failed.
+     */
+    protected Object writeReplace() throws ObjectStreamException {
+        unmarshalRow();
 
-        return this;
+        return tuple;
     }
 }
diff --git 
a/modules/table/src/test/java/org/apache/ignite/internal/table/MutableRowTupleAdapterTest.java
 
b/modules/table/src/test/java/org/apache/ignite/internal/table/MutableRowTupleAdapterTest.java
index c902c1b..f914750 100644
--- 
a/modules/table/src/test/java/org/apache/ignite/internal/table/MutableRowTupleAdapterTest.java
+++ 
b/modules/table/src/test/java/org/apache/ignite/internal/table/MutableRowTupleAdapterTest.java
@@ -17,6 +17,10 @@
 
 package org.apache.ignite.internal.table;
 
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
 import java.math.BigDecimal;
 import java.math.BigInteger;
 import java.time.Instant;
@@ -371,17 +375,182 @@ public class MutableRowTupleAdapterTest {
         checkTuples(schema, tuple, rowTuple);
     }
 
-    private void checkTuples(SchemaDescriptor schema, Tuple tuple, Tuple 
rowTuple) {
+    @Test
+    public void testSerialization() throws Exception {
+        Random rnd = new Random();
+
+        SchemaDescriptor schema = new SchemaDescriptor(tbl.tableId(), 42,
+            new Column[]{new Column("keyUuidCol", NativeTypes.UUID, true)},
+            new Column[]{
+                new Column("valByteCol", INT8, true),
+                new Column("valShortCol", INT16, true),
+                new Column("valIntCol", INT32, true),
+                new Column("valLongCol", INT64, true),
+                new Column("valFloatCol", FLOAT, true),
+                new Column("valDoubleCol", DOUBLE, true),
+                new Column("valDateCol", DATE, true),
+                new Column("valTimeCol", time(), true),
+                new Column("valDateTimeCol", datetime(), true),
+                new Column("valTimeStampCol", timestamp(), true),
+                new Column("valBitmask1Col", NativeTypes.bitmaskOf(22), true),
+                new Column("valBytesCol", BYTES, false),
+                new Column("valStringCol", STRING, false),
+                new Column("valNumberCol", NativeTypes.numberOf(20), false),
+                new Column("valDecimalCol", NativeTypes.decimalOf(25, 5), 
false),
+            }
+        );
+
+        Tuple tup1 = new TupleImpl()
+                         .set("valByteCol", (byte)1)
+                         .set("valShortCol", (short)2)
+                         .set("valIntCol", 3)
+                         .set("valLongCol", 4L)
+                         .set("valFloatCol", 0.055f)
+                         .set("valDoubleCol", 0.066d)
+                         .set("keyUuidCol", UUID.randomUUID())
+                         .set("valDateCol", LocalDate.now())
+                         .set("valDateTimeCol", LocalDateTime.now())
+                         .set("valTimeCol", LocalTime.now())
+                         .set("valTimeStampCol", Instant.now())
+                         .set("valBitmask1Col", randomBitSet(rnd, 12))
+                         .set("valBytesCol", IgniteTestUtils.randomBytes(rnd, 
13))
+                         .set("valStringCol", 
IgniteTestUtils.randomString(rnd, 14))
+                         .set("valNumberCol", 
BigInteger.valueOf(rnd.nextLong()))
+                         .set("valDecimalCol", 
BigDecimal.valueOf(rnd.nextLong(), 5));
+
+        TupleMarshaller marshaller = new TupleMarshallerImpl(null, tbl, new 
DummySchemaManagerImpl(schema));
+
+        Row row = new Row(schema, new 
ByteBufferRow(marshaller.marshal(tup1).bytes()));
+
+        Tuple tup2 = deserializeTuple(serializeTuple(TableRow.tuple(row)));
+
+        assertTupleEquals(tup1, tup2);
+    }
+
+    @Test
+    public void testKeyValueSerialization() throws Exception {
+        Random rnd = new Random();
+
+        SchemaDescriptor schema = new SchemaDescriptor(tbl.tableId(), 42,
+            new Column[]{new Column("keyUuidCol", NativeTypes.UUID, true)},
+            new Column[]{
+                new Column("valByteCol", INT8, true),
+                new Column("valShortCol", INT16, true),
+                new Column("valIntCol", INT32, true),
+                new Column("valLongCol", INT64, true),
+                new Column("valFloatCol", FLOAT, true),
+                new Column("valDoubleCol", DOUBLE, true),
+                new Column("valDateCol", DATE, true),
+                new Column("valTimeCol", time(), true),
+                new Column("valDateTimeCol", datetime(), true),
+                new Column("valTimeStampCol", timestamp(), true),
+                new Column("valBitmask1Col", NativeTypes.bitmaskOf(22), true),
+                new Column("valBytesCol", BYTES, false),
+                new Column("valStringCol", STRING, false),
+                new Column("valNumberCol", NativeTypes.numberOf(20), false),
+                new Column("valDecimalCol", NativeTypes.decimalOf(25, 5), 
false),
+            }
+        );
+
+        Tuple key1 = new TupleImpl().set("keyUuidCol", UUID.randomUUID());
+        Tuple val1 = new TupleImpl()
+                         .set("valByteCol", (byte)1)
+                         .set("valShortCol", (short)2)
+                         .set("valIntCol", 3)
+                         .set("valLongCol", 4L)
+                         .set("valFloatCol", 0.055f)
+                         .set("valDoubleCol", 0.066d)
+                         .set("valDateCol", LocalDate.now())
+                         .set("valDateTimeCol", LocalDateTime.now())
+                         .set("valTimeCol", LocalTime.now())
+                         .set("valTimeStampCol", Instant.now())
+                         .set("valBitmask1Col", randomBitSet(rnd, 12))
+                         .set("valBytesCol", IgniteTestUtils.randomBytes(rnd, 
13))
+                         .set("valStringCol", 
IgniteTestUtils.randomString(rnd, 14))
+                         .set("valNumberCol", 
BigInteger.valueOf(rnd.nextLong()))
+                         .set("valDecimalCol", 
BigDecimal.valueOf(rnd.nextLong(), 5));
+
+        TupleMarshaller marshaller = new TupleMarshallerImpl(null, tbl, new 
DummySchemaManagerImpl(schema));
+
+        Row row = new Row(schema, new ByteBufferRow(marshaller.marshal(key1, 
val1).bytes()));
+
+        Tuple key2 = deserializeTuple(serializeTuple(TableRow.keyTuple(row)));
+        Tuple val2 = 
deserializeTuple(serializeTuple(TableRow.valueTuple(row)));
+
+        assertTupleEquals(key1, key2);
+        assertTupleEquals(val1, val2);
+    }
+
+    /**
+     * Deserializes tuple.
+     *
+     * @param data Tuple bytes.
+     * @return Tuple.
+     * @throws Exception If failed.
+     */
+    private Tuple deserializeTuple(byte[] data) throws Exception {
+        try (ObjectInputStream is = new ObjectInputStream(new 
ByteArrayInputStream(data))) {
+            return (Tuple)is.readObject();
+        }
+    }
+
+    /**
+     * Serailizes tuple.
+     *
+     * @param tup Tuple.
+     * @return Tuple bytes.
+     * @throws Exception If failed.
+     */
+    private byte[] serializeTuple(Tuple tup) throws Exception {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+
+        try (ObjectOutputStream os = new ObjectOutputStream(baos)) {
+            os.writeObject(tup);
+        }
+
+        return baos.toByteArray();
+    }
+
+    /**
+     * Assert that {@code expected} and {@code actual} tuples are equal.
+     *
+     * @param expected Expected tuple.
+     * @param actual Actual tuple.
+     */
+    private void assertTupleEquals(Tuple expected, Tuple actual) {
+        assertEquals(expected.columnCount(), actual.columnCount(), "Tuple size 
mismatch");
+
+        for (int i = 0; i < expected.columnCount(); i++) {
+            String name = expected.columnName(i);
+
+            if (expected.value(i) instanceof byte[]) {
+                assertArrayEquals((byte[])expected.value(i), 
actual.value(actual.columnIndex(name)), "columnIdx=" + i);
+                assertArrayEquals((byte[])expected.value(name), 
actual.value(name), "columnName=" + name);
+            } else {
+                assertEquals((Object)expected.value(i), 
actual.value(actual.columnIndex(name)), "columnIdx=" + i);
+                assertEquals((Object)expected.value(name), actual.value(name), 
"columnName=" + name);
+            }
+        }
+    }
+
+    /**
+     * Check tuple column accessors.
+     *
+     * @param schema Schema to check against.
+     * @param expected Tuple with expected values.
+     * @param actual Tuple to check.
+     */
+    private void checkTuples(SchemaDescriptor schema, Tuple expected, Tuple 
actual) {
         for (int i = 0; i < schema.length(); i++) {
             Column col = schema.column(i);
             String name = col.name();
 
             if (col.type().spec() == NativeTypeSpec.BYTES) {
-                
assertArrayEquals((byte[])tuple.value(tuple.columnIndex(name)), 
rowTuple.value(rowTuple.columnIndex(name)), "columnIdx=" + i);
-                assertArrayEquals((byte[])tuple.value(name), 
rowTuple.value(name), "columnName=" + name);
+                
assertArrayEquals((byte[])expected.value(expected.columnIndex(name)), 
actual.value(actual.columnIndex(name)), "columnIdx=" + i);
+                assertArrayEquals((byte[])expected.value(name), 
actual.value(name), "columnName=" + name);
             } else {
-                assertEquals((Object)tuple.value(tuple.columnIndex(name)), 
rowTuple.value(rowTuple.columnIndex(name)), "columnIdx=" + i);
-                assertEquals((Object)tuple.value(name), rowTuple.value(name), 
"columnName=" + name);
+                
assertEquals((Object)expected.value(expected.columnIndex(name)), 
actual.value(actual.columnIndex(name)), "columnIdx=" + i);
+                assertEquals((Object)expected.value(name), actual.value(name), 
"columnName=" + name);
             }
         }
     }
diff --git 
a/modules/table/src/test/java/org/apache/ignite/internal/table/impl/TestTupleBuilder.java
 
b/modules/table/src/test/java/org/apache/ignite/internal/table/impl/TestTupleBuilder.java
index f83cf2a..50b3933 100644
--- 
a/modules/table/src/test/java/org/apache/ignite/internal/table/impl/TestTupleBuilder.java
+++ 
b/modules/table/src/test/java/org/apache/ignite/internal/table/impl/TestTupleBuilder.java
@@ -39,19 +39,19 @@ public class TestTupleBuilder implements Tuple {
     private final Map<String, Object> map = new HashMap<>();
 
     /** {@inheritDoc} */
-    @Override public TestTupleBuilder set(String columnName, Object value) {
+    @Override public TestTupleBuilder set(@NotNull String columnName, Object 
value) {
         map.put(columnName, value);
 
         return this;
     }
 
     /** {@inheritDoc} */
-    @Override public <T> T valueOrDefault(String columnName, T def) {
+    @Override public <T> T valueOrDefault(@NotNull String columnName, T def) {
         return (T)map.getOrDefault(columnName, def);
     }
 
     /** {@inheritDoc} */
-    @Override public <T> T value(String columnName) {
+    @Override public <T> T value(@NotNull String columnName) {
         return (T)map.get(columnName);
     }
 
@@ -71,12 +71,12 @@ public class TestTupleBuilder implements Tuple {
     }
 
     /** {@inheritDoc} */
-    @Override public int columnIndex(String columnName) {
+    @Override public int columnIndex(@NotNull String columnName) {
         throw new UnsupportedOperationException();
     }
 
     /** {@inheritDoc} */
-    @Override public BinaryObject binaryObjectValue(String columnName) {
+    @Override public BinaryObject binaryObjectValue(@NotNull String 
columnName) {
         byte[] data = value(columnName);
 
         return BinaryObjects.wrap(data);
@@ -88,7 +88,7 @@ public class TestTupleBuilder implements Tuple {
     }
 
     /** {@inheritDoc} */
-    @Override public byte byteValue(String columnName) {
+    @Override public byte byteValue(@NotNull String columnName) {
         return value(columnName);
     }
 
@@ -98,7 +98,7 @@ public class TestTupleBuilder implements Tuple {
     }
 
     /** {@inheritDoc} */
-    @Override public short shortValue(String columnName) {
+    @Override public short shortValue(@NotNull String columnName) {
         return value(columnName);
     }
 
@@ -108,7 +108,7 @@ public class TestTupleBuilder implements Tuple {
     }
 
     /** {@inheritDoc} */
-    @Override public int intValue(String columnName) {
+    @Override public int intValue(@NotNull String columnName) {
         return value(columnName);
     }
 
@@ -118,7 +118,7 @@ public class TestTupleBuilder implements Tuple {
     }
 
     /** {@inheritDoc} */
-    @Override public long longValue(String columnName) {
+    @Override public long longValue(@NotNull String columnName) {
         return value(columnName);
     }
 
@@ -128,7 +128,7 @@ public class TestTupleBuilder implements Tuple {
     }
 
     /** {@inheritDoc} */
-    @Override public float floatValue(String columnName) {
+    @Override public float floatValue(@NotNull String columnName) {
         return value(columnName);
     }
 
@@ -138,7 +138,7 @@ public class TestTupleBuilder implements Tuple {
     }
 
     /** {@inheritDoc} */
-    @Override public double doubleValue(String columnName) {
+    @Override public double doubleValue(@NotNull String columnName) {
         return value(columnName);
     }
 
@@ -148,7 +148,7 @@ public class TestTupleBuilder implements Tuple {
     }
 
     /** {@inheritDoc} */
-    @Override public String stringValue(String columnName) {
+    @Override public String stringValue(@NotNull String columnName) {
         return value(columnName);
     }
 
@@ -158,7 +158,7 @@ public class TestTupleBuilder implements Tuple {
     }
 
     /** {@inheritDoc} */
-    @Override public UUID uuidValue(String columnName) {
+    @Override public UUID uuidValue(@NotNull String columnName) {
         return value(columnName);
     }
 
@@ -168,7 +168,7 @@ public class TestTupleBuilder implements Tuple {
     }
 
     /** {@inheritDoc} */
-    @Override public BitSet bitmaskValue(String columnName) {
+    @Override public BitSet bitmaskValue(@NotNull String columnName) {
         return value(columnName);
     }
 
diff --git 
a/modules/table/src/test/java/org/apache/ignite/table/TupleImplTest.java 
b/modules/table/src/test/java/org/apache/ignite/table/TupleImplTest.java
index 9841602..ef32e7d 100644
--- a/modules/table/src/test/java/org/apache/ignite/table/TupleImplTest.java
+++ b/modules/table/src/test/java/org/apache/ignite/table/TupleImplTest.java
@@ -17,6 +17,11 @@
 
 package org.apache.ignite.table;
 
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
 import java.math.BigDecimal;
 import java.math.BigInteger;
 import java.time.Instant;
@@ -36,13 +41,13 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
 
 /**
  * Tests server tuple builder implementation.
- *
+ * <p>
  * Should be in sync with org.apache.ignite.client.ClientTupleBuilderTest.
  */
 public class TupleImplTest {
     @Test
     public void testValueReturnsValueByName() {
-        assertEquals(3L, (Long) getTuple().value("id"));
+        assertEquals(3L, (Long)getTuple().value("id"));
         assertEquals("Shirt", getTuple().value("name"));
     }
 
@@ -54,7 +59,7 @@ public class TupleImplTest {
 
     @Test
     public void testValueReturnsValueByIndex() {
-        assertEquals(3L, (Long) getTuple().value(0));
+        assertEquals(3L, (Long)getTuple().value(0));
         assertEquals("Shirt", getTuple().value(1));
     }
 
@@ -169,13 +174,60 @@ public class TupleImplTest {
         }
     }
 
+    @Test
+    public void testSerialization() throws IOException, ClassNotFoundException 
{
+        Random rnd = new Random();
+
+        Tuple tup1 = new TupleImpl()
+                         .set("valByteCol", (byte)1)
+                         .set("valShortCol", (short)2)
+                         .set("valIntCol", 3)
+                         .set("valLongCol", 4L)
+                         .set("valFloatCol", 0.055f)
+                         .set("valDoubleCol", 0.066d)
+                         .set("keyUuidCol", UUID.randomUUID())
+                         .set("valDateCol", LocalDate.now())
+                         .set("valDateTimeCol", LocalDateTime.now())
+                         .set("valTimeCol", LocalTime.now())
+                         .set("valTimeStampCol", Instant.now())
+                         .set("valBitmask1Col", randomBitSet(rnd, 12))
+                         .set("valBytesCol", IgniteTestUtils.randomBytes(rnd, 
13))
+                         .set("valStringCol", 
IgniteTestUtils.randomString(rnd, 14))
+                         .set("valNumberCol", 
BigInteger.valueOf(rnd.nextLong()))
+                         .set("valDecimalCol", 
BigDecimal.valueOf(rnd.nextLong(), 5));
+
+        Tuple tup2;
+
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+
+        try (ObjectOutputStream os = new ObjectOutputStream(baos)) {
+            os.writeObject(tup1);
+        }
+
+        try (ObjectInputStream is = new ObjectInputStream(new 
ByteArrayInputStream(baos.toByteArray()))) {
+            tup2 = (Tuple)is.readObject();
+        }
+
+        for (int i = 0; i < tup1.columnCount(); i++) {
+            String name = tup1.columnName(i);
+
+            if (tup1.value(i) instanceof byte[]) {
+                assertArrayEquals((byte[])tup1.value(i), tup2.value(i), 
"columnIdx=" + i);
+                assertArrayEquals((byte[])tup1.value(name), tup2.value(name), 
"columnName=" + name);
+            } else {
+                assertEquals((Object)tup1.value(i), tup1.value(i), 
"columnIdx=" + i);
+                assertEquals((Object)tup1.value(name), tup1.value(name), 
"columnName=" + name);
+            }
+        }
+    }
+
     private static TupleImpl createTuple() {
         return new TupleImpl();
     }
 
     private static Tuple getTuple() {
         return new TupleImpl()
-                .set("id", 3L)
-                .set("name", "Shirt");
+                   .set("id", 3L)
+                   .set("name", "Shirt");
     }
 }

Reply via email to