You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@kudu.apache.org by al...@apache.org on 2022/10/20 01:42:08 UTC

[kudu] branch master updated: KUDU-3353 [schema] Add an immutable attribute on column schema (part 3)

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

alexey pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/kudu.git


The following commit(s) were added to refs/heads/master by this push:
     new f20dcf57a KUDU-3353 [schema] Add an immutable attribute on column schema (part 3)
f20dcf57a is described below

commit f20dcf57ad76e9b1bb57fe60b27ea3a8f02df233
Author: Yingchun Lai <la...@apache.org>
AuthorDate: Tue Sep 13 19:40:35 2022 +0800

    KUDU-3353 [schema] Add an immutable attribute on column schema (part 3)
    
    This is a follow-up to b6eedb224f715ad86378a92d25f09c2084b0e2b7.
    This patch contains the Java client-side changes
    of the "new column attribute IMMUTABLE" feature,
    including:
    1. Adds a new 'immutable(boolean immutable)' method to
       class ColumnSchemaBuilder to add/remove IMMUTABLE
       attribute to/from a column.
    2. Adds a new 'isImmutable()' method to class
       ColumnSchema to check if the attribute is set for
       a column schema.
    3. Adds a new 'hasImmutableColumns()' method to class
       Schema to check if there's at least one immutable
       column for a table schema.
    4. Adds a new 'changeImmutable(String name, boolean immutable)'
       method to class AlterTableOptions to change the
       immutable attribute for a column.
    5. Adds a new UpsertIgnore operation in the client API:
       use the newly added KuduTable.newUpsertIgnore() to
       create a new instance of such operation.
       Both UpsertIgnore and UpdateIgnore operations can be used
       to ignore errors on updating cells of immutable columns.
    6. Adds unit tests to cover the newly introduced functionality.
    
    Change-Id: Ifdfdcd123296803a3b5e856ec5eaac49c05b7f8d
    Reviewed-on: http://gerrit.cloudera.org:8080/18993
    Tested-by: Alexey Serbin <al...@apache.org>
    Reviewed-by: Alexey Serbin <al...@apache.org>
---
 .../main/java/org/apache/kudu/ColumnSchema.java    |  28 ++-
 .../src/main/java/org/apache/kudu/Schema.java      |  12 ++
 .../org/apache/kudu/client/AlterTableOptions.java  |  51 ++++-
 .../org/apache/kudu/client/AsyncKuduClient.java    |   5 +-
 .../org/apache/kudu/client/CreateTableOptions.java |  23 +-
 .../org/apache/kudu/client/CreateTableRequest.java |   2 +-
 .../java/org/apache/kudu/client/KuduClient.java    |   3 +-
 .../java/org/apache/kudu/client/KuduTable.java     |  13 +-
 .../java/org/apache/kudu/client/Operation.java     |   3 +-
 .../org/apache/kudu/client/ProtobufHelper.java     |   2 +
 .../main/java/org/apache/kudu/client/Status.java   |  13 ++
 .../java/org/apache/kudu/client/UpdateIgnore.java  |   3 +-
 .../{UpdateIgnore.java => UpsertIgnore.java}       |   9 +-
 .../org/apache/kudu/client/TestAlterTable.java     |  32 +++
 .../org/apache/kudu/client/TestKuduSession.java    | 240 +++++++++++++++++++--
 .../java/org/apache/kudu/client/TestKuduTable.java |  73 +++++++
 .../java/org/apache/kudu/test/ClientTestUtil.java  |   7 +
 17 files changed, 476 insertions(+), 43 deletions(-)

diff --git a/java/kudu-client/src/main/java/org/apache/kudu/ColumnSchema.java b/java/kudu-client/src/main/java/org/apache/kudu/ColumnSchema.java
index 933aa6647..62e399550 100644
--- a/java/kudu-client/src/main/java/org/apache/kudu/ColumnSchema.java
+++ b/java/kudu-client/src/main/java/org/apache/kudu/ColumnSchema.java
@@ -40,6 +40,7 @@ public class ColumnSchema {
   private final Type type;
   private final boolean key;
   private final boolean nullable;
+  private final boolean immutable;
   private final Object defaultValue;
   private final int desiredBlockSize;
   private final Encoding encoding;
@@ -103,7 +104,7 @@ public class ColumnSchema {
     }
   }
 
-  private ColumnSchema(String name, Type type, boolean key, boolean nullable,
+  private ColumnSchema(String name, Type type, boolean key, boolean nullable, boolean immutable,
                        Object defaultValue, int desiredBlockSize, Encoding encoding,
                        CompressionAlgorithm compressionAlgorithm,
                        ColumnTypeAttributes typeAttributes, Common.DataType wireType,
@@ -112,6 +113,7 @@ public class ColumnSchema {
     this.type = type;
     this.key = key;
     this.nullable = nullable;
+    this.immutable = immutable;
     this.defaultValue = defaultValue;
     this.desiredBlockSize = desiredBlockSize;
     this.encoding = encoding;
@@ -154,6 +156,14 @@ public class ColumnSchema {
     return nullable;
   }
 
+  /**
+   * Answers if the column is immutable
+   * @return true if it is immutable, else false
+   */
+  public boolean isImmutable() {
+    return immutable;
+  }
+
   /**
    * The Java object representation of the default value that's read
    * @return the default read value
@@ -267,6 +277,7 @@ public class ColumnSchema {
     private final Type type;
     private boolean key = false;
     private boolean nullable = false;
+    private boolean immutable = false;
     private Object defaultValue = null;
     private int desiredBlockSize = 0;
     private Encoding encoding = null;
@@ -294,6 +305,7 @@ public class ColumnSchema {
       this.type = that.type;
       this.key = that.key;
       this.nullable = that.nullable;
+      this.immutable = that.immutable;
       this.defaultValue = that.defaultValue;
       this.desiredBlockSize = that.desiredBlockSize;
       this.encoding = that.encoding;
@@ -330,6 +342,17 @@ public class ColumnSchema {
       return this;
     }
 
+    /**
+     * Marks the column as immutable or not. False by default.
+     *
+     * @param immutable a boolean that indicates if the column is immutable
+     * @return this instance
+     */
+    public ColumnSchemaBuilder immutable(boolean immutable) {
+      this.immutable = immutable;
+      return this;
+    }
+
     /**
      * Sets the default value that will be read from the column. Null by default.
      * @param defaultValue a Java object representation of the default value that's read
@@ -432,8 +455,9 @@ public class ColumnSchema {
                           CharUtil.MIN_VARCHAR_LENGTH, CharUtil.MAX_VARCHAR_LENGTH));
         }
       }
+
       return new ColumnSchema(name, type,
-                              key, nullable, defaultValue,
+                              key, nullable, immutable, defaultValue,
                               desiredBlockSize, encoding, compressionAlgorithm,
                               typeAttributes, wireType, comment);
     }
diff --git a/java/kudu-client/src/main/java/org/apache/kudu/Schema.java b/java/kudu-client/src/main/java/org/apache/kudu/Schema.java
index 3ba1b850a..f07d0c616 100644
--- a/java/kudu-client/src/main/java/org/apache/kudu/Schema.java
+++ b/java/kudu-client/src/main/java/org/apache/kudu/Schema.java
@@ -72,6 +72,7 @@ public class Schema {
   private final int varLengthColumnCount;
   private final int rowSize;
   private final boolean hasNullableColumns;
+  private final boolean hasImmutableColumns;
 
   private final int isDeletedIndex;
   private static final int NO_IS_DELETED_INDEX = -1;
@@ -113,6 +114,7 @@ public class Schema {
     this.columnIdByName = hasColumnIds ? new HashMap<>(columnIds.size()) : null;
     int offset = 0;
     boolean hasNulls = false;
+    boolean hasImmutables = false;
     int isDeletedIndex = NO_IS_DELETED_INDEX;
     // pre-compute a few counts and offsets
     for (int index = 0; index < columns.size(); index++) {
@@ -122,6 +124,7 @@ public class Schema {
       }
 
       hasNulls |= column.isNullable();
+      hasImmutables |= column.isImmutable();
       columnOffsets[index] = offset;
       offset += column.getTypeSize();
       if (this.columnsByName.put(column.getName(), index) != null) {
@@ -152,6 +155,7 @@ public class Schema {
     this.varLengthColumnCount = varLenCnt;
     this.rowSize = getRowSize(this.columnsByIndex);
     this.hasNullableColumns = hasNulls;
+    this.hasImmutableColumns = hasImmutables;
     this.isDeletedIndex = isDeletedIndex;
   }
 
@@ -306,6 +310,14 @@ public class Schema {
     return this.hasNullableColumns;
   }
 
+  /**
+   * Tells if there's at least one immutable column
+   * @return true if at least one column is immutable, else false.
+   */
+  public boolean hasImmutableColumns() {
+    return this.hasImmutableColumns;
+  }
+
   /**
    * Tells whether this schema includes IDs for columns. A schema created by a client as part of
    * table creation will not include IDs, but schemas for open tables will include IDs.
diff --git a/java/kudu-client/src/main/java/org/apache/kudu/client/AlterTableOptions.java b/java/kudu-client/src/main/java/org/apache/kudu/client/AlterTableOptions.java
index e5e8f193f..026c3f6ce 100644
--- a/java/kudu-client/src/main/java/org/apache/kudu/client/AlterTableOptions.java
+++ b/java/kudu-client/src/main/java/org/apache/kudu/client/AlterTableOptions.java
@@ -21,6 +21,7 @@ import static org.apache.kudu.ColumnSchema.CompressionAlgorithm;
 import static org.apache.kudu.ColumnSchema.Encoding;
 import static org.apache.kudu.master.Master.AlterTableRequestPB;
 
+import java.util.ArrayList;
 import java.util.EnumSet;
 import java.util.List;
 import java.util.Map;
@@ -477,6 +478,24 @@ public class AlterTableOptions {
     return this;
   }
 
+  /**
+   * Change the immutable attribute for the column.
+   *
+   * @param name name of the column
+   * @param immutable the new immutable attribute for the column.
+   * @return this instance
+   */
+  public AlterTableOptions changeImmutable(String name, boolean immutable) {
+    AlterTableRequestPB.Step.Builder step = pb.addAlterSchemaStepsBuilder();
+    step.setType(AlterTableRequestPB.StepType.ALTER_COLUMN);
+    AlterTableRequestPB.AlterColumn.Builder alterBuilder =
+            AlterTableRequestPB.AlterColumn.newBuilder();
+    alterBuilder.setDelta(
+            Common.ColumnSchemaDeltaPB.newBuilder().setName(name).setImmutable(immutable));
+    step.setAlterColumn(alterBuilder);
+    return this;
+  }
+
   /**
    * Change the table's extra configuration properties.
    * These configuration properties will be merged into existing configuration properties.
@@ -532,14 +551,30 @@ public class AlterTableOptions {
   }
 
   List<Integer> getRequiredFeatureFlags() {
-    if (!hasAddDropRangePartitions()) {
-      return ImmutableList.of();
+    boolean hasImmutables = false;
+    for (AlterTableRequestPB.Step.Builder step : pb.getAlterSchemaStepsBuilderList()) {
+      if ((step.getType() == AlterTableRequestPB.StepType.ADD_COLUMN &&
+           step.getAddColumn().getSchema().hasImmutable()) ||
+          (step.getType() == AlterTableRequestPB.StepType.ALTER_COLUMN &&
+           step.getAlterColumn().getDelta().hasImmutable())) {
+        hasImmutables = true;
+        break;
+      }
+    }
+
+    List<Integer> requiredFeatureFlags = new ArrayList<>();
+    if (hasImmutables) {
+      requiredFeatureFlags.add(
+              Integer.valueOf(Master.MasterFeatures.IMMUTABLE_COLUMN_ATTRIBUTE_VALUE));
     }
-    if (!isAddingRangeWithCustomHashSchema) {
-      return ImmutableList.of(Master.MasterFeatures.RANGE_PARTITION_BOUNDS_VALUE);
+
+    if (hasAddDropRangePartitions()) {
+      requiredFeatureFlags.add(Integer.valueOf(Master.MasterFeatures.RANGE_PARTITION_BOUNDS_VALUE));
+      if (isAddingRangeWithCustomHashSchema) {
+        requiredFeatureFlags.add(
+                Integer.valueOf(Master.MasterFeatures.RANGE_SPECIFIC_HASH_SCHEMA_VALUE));
+      }
     }
-    return ImmutableList.of(
-        Master.MasterFeatures.RANGE_PARTITION_BOUNDS_VALUE,
-        Master.MasterFeatures.RANGE_SPECIFIC_HASH_SCHEMA_VALUE);
+    return requiredFeatureFlags;
   }
-}
\ No newline at end of file
+}
diff --git a/java/kudu-client/src/main/java/org/apache/kudu/client/AsyncKuduClient.java b/java/kudu-client/src/main/java/org/apache/kudu/client/AsyncKuduClient.java
index d21ff80fd..837c9f3c1 100644
--- a/java/kudu-client/src/main/java/org/apache/kudu/client/AsyncKuduClient.java
+++ b/java/kudu-client/src/main/java/org/apache/kudu/client/AsyncKuduClient.java
@@ -2771,7 +2771,8 @@ public class AsyncKuduClient implements AutoCloseable {
   }
 
   /**
-   * Sends a request to the master to check if the cluster supports ignore operations.
+   * Sends a request to the master to check if the cluster supports ignore operations, including
+   * InsertIgnore, UpdateIgnore and DeleteIgnore operations.
    * @return true if the cluster supports ignore operations
    */
   @InterfaceAudience.Private
@@ -2783,8 +2784,6 @@ public class AsyncKuduClient implements AutoCloseable {
     return AsyncUtil.addBoth(response, new PingSupportsFeatureCallback());
   }
 
-  // TODO(yingchun): also need add 'public Deferred<Boolean> supportsUpsertIgnoreOperations()'
-
   private static final class PingSupportsFeatureCallback implements Callback<Boolean, Object> {
     @Override
     public Boolean call(final Object resp) {
diff --git a/java/kudu-client/src/main/java/org/apache/kudu/client/CreateTableOptions.java b/java/kudu-client/src/main/java/org/apache/kudu/client/CreateTableOptions.java
index 73e14c215..5e3547eea 100644
--- a/java/kudu-client/src/main/java/org/apache/kudu/client/CreateTableOptions.java
+++ b/java/kudu-client/src/main/java/org/apache/kudu/client/CreateTableOptions.java
@@ -17,6 +17,7 @@
 
 package org.apache.kudu.client;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 
@@ -27,6 +28,7 @@ import org.apache.yetus.audience.InterfaceAudience;
 import org.apache.yetus.audience.InterfaceStability;
 
 import org.apache.kudu.Common;
+import org.apache.kudu.Schema;
 import org.apache.kudu.master.Master;
 
 /**
@@ -333,16 +335,21 @@ public class CreateTableOptions {
     return pb;
   }
 
-  List<Integer> getRequiredFeatureFlags() {
-    if (rangePartitions.isEmpty() && customRangePartitions.isEmpty()) {
-      return ImmutableList.of();
+  List<Integer> getRequiredFeatureFlags(Schema schema) {
+    List<Integer> requiredFeatureFlags = new ArrayList<>();
+    if (schema.hasImmutableColumns()) {
+      requiredFeatureFlags.add(
+              Integer.valueOf(Master.MasterFeatures.IMMUTABLE_COLUMN_ATTRIBUTE_VALUE));
     }
-    if (customRangePartitions.isEmpty()) {
-      return ImmutableList.of(Master.MasterFeatures.RANGE_PARTITION_BOUNDS_VALUE);
+    if (!rangePartitions.isEmpty() || !customRangePartitions.isEmpty()) {
+      requiredFeatureFlags.add(Integer.valueOf(Master.MasterFeatures.RANGE_PARTITION_BOUNDS_VALUE));
+    }
+    if (!customRangePartitions.isEmpty()) {
+      requiredFeatureFlags.add(
+              Integer.valueOf(Master.MasterFeatures.RANGE_SPECIFIC_HASH_SCHEMA_VALUE));
     }
-    return ImmutableList.of(
-        Master.MasterFeatures.RANGE_PARTITION_BOUNDS_VALUE,
-        Master.MasterFeatures.RANGE_SPECIFIC_HASH_SCHEMA_VALUE);
+
+    return requiredFeatureFlags;
   }
 
   boolean shouldWait() {
diff --git a/java/kudu-client/src/main/java/org/apache/kudu/client/CreateTableRequest.java b/java/kudu-client/src/main/java/org/apache/kudu/client/CreateTableRequest.java
index 4f6a07567..633bfad4d 100644
--- a/java/kudu-client/src/main/java/org/apache/kudu/client/CreateTableRequest.java
+++ b/java/kudu-client/src/main/java/org/apache/kudu/client/CreateTableRequest.java
@@ -53,7 +53,7 @@ class CreateTableRequest extends KuduRpc<CreateTableResponse> {
     this.schema = schema;
     this.name = name;
     this.builder = cto.getBuilder();
-    featureFlags = cto.getRequiredFeatureFlags();
+    featureFlags = cto.getRequiredFeatureFlags(schema);
   }
 
   @Override
diff --git a/java/kudu-client/src/main/java/org/apache/kudu/client/KuduClient.java b/java/kudu-client/src/main/java/org/apache/kudu/client/KuduClient.java
index 205167fa2..382cce9e2 100644
--- a/java/kudu-client/src/main/java/org/apache/kudu/client/KuduClient.java
+++ b/java/kudu-client/src/main/java/org/apache/kudu/client/KuduClient.java
@@ -488,7 +488,8 @@ public class KuduClient implements AutoCloseable {
   }
 
   /**
-   * Sends a request to the master to check if the cluster supports ignore operations.
+   * Sends a request to the master to check if the cluster supports ignore operations, including
+   * InsertIgnore, UpdateIgnore and DeleteIgnore operations.
    * @return true if the cluster supports ignore operations
    */
   @InterfaceAudience.Private
diff --git a/java/kudu-client/src/main/java/org/apache/kudu/client/KuduTable.java b/java/kudu-client/src/main/java/org/apache/kudu/client/KuduTable.java
index 5dd06f789..1346231f3 100644
--- a/java/kudu-client/src/main/java/org/apache/kudu/client/KuduTable.java
+++ b/java/kudu-client/src/main/java/org/apache/kudu/client/KuduTable.java
@@ -190,6 +190,16 @@ public class KuduTable {
     return new Upsert(this);
   }
 
+  /**
+   * Get a new upsert ignore configured with this table's schema. The operation ignores errors of
+   * updating immutable cells in a row. This is useful when upserting rows in a table with immutable
+   * columns.
+   * @return an upsert with this table's schema
+   */
+  public UpsertIgnore newUpsertIgnore() {
+    return new UpsertIgnore(this);
+  }
+
   /**
    * Get a new insert ignore configured with this table's schema. An insert ignore will
    * ignore duplicate row errors. This is useful when the same insert may be sent multiple times.
@@ -202,7 +212,8 @@ public class KuduTable {
 
   /**
    * Get a new update ignore configured with this table's schema. An update ignore will
-   * ignore missing row errors. This is useful to update a row only if it exists.
+   * ignore missing row errors and updating on immutable columns errors. This is useful to
+   * update a row only if it exists, or update a row with immutable columns.
    * The returned object should not be reused.
    * @return an update ignore with this table's schema
    */
diff --git a/java/kudu-client/src/main/java/org/apache/kudu/client/Operation.java b/java/kudu-client/src/main/java/org/apache/kudu/client/Operation.java
index fdd636a31..81d7b0efe 100644
--- a/java/kudu-client/src/main/java/org/apache/kudu/client/Operation.java
+++ b/java/kudu-client/src/main/java/org/apache/kudu/client/Operation.java
@@ -74,7 +74,8 @@ public abstract class Operation extends KuduRpc<OperationResponse> {
         (byte) RowOperationsPB.Type.INCLUSIVE_RANGE_UPPER_BOUND.getNumber()),
     INSERT_IGNORE((byte) RowOperationsPB.Type.INSERT_IGNORE.getNumber()),
     UPDATE_IGNORE((byte) RowOperationsPB.Type.UPDATE_IGNORE.getNumber()),
-    DELETE_IGNORE((byte) RowOperationsPB.Type.DELETE_IGNORE.getNumber());
+    DELETE_IGNORE((byte) RowOperationsPB.Type.DELETE_IGNORE.getNumber()),
+    UPSERT_IGNORE((byte) RowOperationsPB.Type.UPSERT_IGNORE.getNumber());
 
     ChangeType(byte encodedByte) {
       this.encodedByte = encodedByte;
diff --git a/java/kudu-client/src/main/java/org/apache/kudu/client/ProtobufHelper.java b/java/kudu-client/src/main/java/org/apache/kudu/client/ProtobufHelper.java
index c9d94e7af..b7e0f77c6 100644
--- a/java/kudu-client/src/main/java/org/apache/kudu/client/ProtobufHelper.java
+++ b/java/kudu-client/src/main/java/org/apache/kudu/client/ProtobufHelper.java
@@ -107,6 +107,7 @@ public class ProtobufHelper {
         .setType(column.getWireType())
         .setIsKey(column.isKey())
         .setIsNullable(column.isNullable())
+        .setImmutable(column.isImmutable())
         .setCfileBlockSize(column.getDesiredBlockSize());
 
     if (!flags.contains(SchemaPBConversionFlags.SCHEMA_PB_WITHOUT_ID) && colId >= 0) {
@@ -161,6 +162,7 @@ public class ProtobufHelper {
     return new ColumnSchema.ColumnSchemaBuilder(pb.getName(), type)
                            .key(pb.getIsKey())
                            .nullable(pb.getIsNullable())
+                           .immutable(pb.getImmutable())
                            .defaultValue(defaultValue)
                            .encoding(encoding)
                            .compressionAlgorithm(compressionAlgorithm)
diff --git a/java/kudu-client/src/main/java/org/apache/kudu/client/Status.java b/java/kudu-client/src/main/java/org/apache/kudu/client/Status.java
index 04557557a..6fa227683 100644
--- a/java/kudu-client/src/main/java/org/apache/kudu/client/Status.java
+++ b/java/kudu-client/src/main/java/org/apache/kudu/client/Status.java
@@ -242,6 +242,13 @@ public class Status {
   public static Status EndOfFile(String msg, int posixCode) {
     return new Status(WireProtocol.AppStatusPB.ErrorCode.END_OF_FILE, msg, posixCode);
   }
+
+  public static Status Immutable(String msg) {
+    return new Status(WireProtocol.AppStatusPB.ErrorCode.IMMUTABLE, msg);
+  }
+  public static Status Immutable(String msg, int posixCode) {
+    return new Status(WireProtocol.AppStatusPB.ErrorCode.IMMUTABLE, msg, posixCode);
+  }
   // CHECKSTYLE:ON
   // Boolean status checks.
 
@@ -321,6 +328,10 @@ public class Status {
     return code == WireProtocol.AppStatusPB.ErrorCode.END_OF_FILE;
   }
 
+  public boolean isImmutable() {
+    return code == WireProtocol.AppStatusPB.ErrorCode.IMMUTABLE;
+  }
+
   /**
    * Return a human-readable version of the status code.
    * See also status.cc in the C++ codebase.
@@ -365,6 +376,8 @@ public class Status {
         return "Incomplete";
       case WireProtocol.AppStatusPB.ErrorCode.END_OF_FILE_VALUE:
         return "End of file";
+      case WireProtocol.AppStatusPB.ErrorCode.IMMUTABLE_VALUE:
+        return "Immutable";
       default:
         return "Unknown error (" + code.getNumber() + ")";
     }
diff --git a/java/kudu-client/src/main/java/org/apache/kudu/client/UpdateIgnore.java b/java/kudu-client/src/main/java/org/apache/kudu/client/UpdateIgnore.java
index 803af32d0..384d3b745 100644
--- a/java/kudu-client/src/main/java/org/apache/kudu/client/UpdateIgnore.java
+++ b/java/kudu-client/src/main/java/org/apache/kudu/client/UpdateIgnore.java
@@ -21,7 +21,8 @@ import org.apache.yetus.audience.InterfaceAudience;
 import org.apache.yetus.audience.InterfaceStability;
 
 /**
- * Represents a single row update ignoring missing rows.
+ * Represents a single row update ignoring missing rows errors and
+ * errors on updating immutable cells.
  * Instances of this class should not be reused.
  */
 @InterfaceAudience.Public
diff --git a/java/kudu-client/src/main/java/org/apache/kudu/client/UpdateIgnore.java b/java/kudu-client/src/main/java/org/apache/kudu/client/UpsertIgnore.java
similarity index 84%
copy from java/kudu-client/src/main/java/org/apache/kudu/client/UpdateIgnore.java
copy to java/kudu-client/src/main/java/org/apache/kudu/client/UpsertIgnore.java
index 803af32d0..c5cff726c 100644
--- a/java/kudu-client/src/main/java/org/apache/kudu/client/UpdateIgnore.java
+++ b/java/kudu-client/src/main/java/org/apache/kudu/client/UpsertIgnore.java
@@ -21,19 +21,20 @@ import org.apache.yetus.audience.InterfaceAudience;
 import org.apache.yetus.audience.InterfaceStability;
 
 /**
- * Represents a single row update ignoring missing rows.
+ * Represents a single row upsert ignoring errors on updating
+ * immutable cells.
  * Instances of this class should not be reused.
  */
 @InterfaceAudience.Public
 @InterfaceStability.Evolving
-public class UpdateIgnore extends Operation {
+public class UpsertIgnore extends Operation {
 
-  UpdateIgnore(KuduTable table) {
+  UpsertIgnore(KuduTable table) {
     super(table);
   }
 
   @Override
   ChangeType getChangeType() {
-    return ChangeType.UPDATE_IGNORE;
+    return ChangeType.UPSERT_IGNORE;
   }
 }
diff --git a/java/kudu-client/src/test/java/org/apache/kudu/client/TestAlterTable.java b/java/kudu-client/src/test/java/org/apache/kudu/client/TestAlterTable.java
index c10b6402d..4654517c9 100644
--- a/java/kudu-client/src/test/java/org/apache/kudu/client/TestAlterTable.java
+++ b/java/kudu-client/src/test/java/org/apache/kudu/client/TestAlterTable.java
@@ -1092,4 +1092,36 @@ public class TestAlterTable {
     table = client.openTable(table.getName());
     assertEquals(newComment, table.getComment());
   }
+
+  @Test
+  public void testAlterAddAndRemoveImmutableAttribute() throws Exception {
+    KuduTable table = createTable(ImmutableList.of());
+    insertRows(table, 0, 100);
+    assertEquals(100, countRowsInTable(table));
+
+    client.alterTable(tableName, new AlterTableOptions()
+            .changeImmutable("c1", true));
+    table = client.openTable(table.getName());
+    assertTrue(table.getSchema().getColumn("c1").isImmutable());
+
+    insertRows(table, 100, 200);
+    assertEquals(200, countRowsInTable(table));
+
+    client.alterTable(tableName, new AlterTableOptions()
+            .changeImmutable("c1", false));
+    table = client.openTable(table.getName());
+    assertFalse(table.getSchema().getColumn("c1").isImmutable());
+
+    insertRows(table, 200, 300);
+    assertEquals(300, countRowsInTable(table));
+
+    final ColumnSchema immu_col = new ColumnSchema.ColumnSchemaBuilder("immu_col", Type.INT32)
+        .nullable(true).immutable(true).build();
+    client.alterTable(tableName, new AlterTableOptions().addColumn(immu_col));
+    table = client.openTable(table.getName());
+    assertTrue(table.getSchema().getColumn("immu_col").isImmutable());
+
+    insertRows(table, 300, 400);
+    assertEquals(400, countRowsInTable(table));
+  }
 }
diff --git a/java/kudu-client/src/test/java/org/apache/kudu/client/TestKuduSession.java b/java/kudu-client/src/test/java/org/apache/kudu/client/TestKuduSession.java
index 422c2e6bb..723d4eba9 100644
--- a/java/kudu-client/src/test/java/org/apache/kudu/client/TestKuduSession.java
+++ b/java/kudu-client/src/test/java/org/apache/kudu/client/TestKuduSession.java
@@ -19,6 +19,7 @@ package org.apache.kudu.client;
 
 import static org.apache.kudu.test.ClientTestUtil.countRowsInScan;
 import static org.apache.kudu.test.ClientTestUtil.createBasicSchemaInsert;
+import static org.apache.kudu.test.ClientTestUtil.createSchemaWithImmutableColumns;
 import static org.apache.kudu.test.ClientTestUtil.getBasicCreateTableOptions;
 import static org.apache.kudu.test.ClientTestUtil.getBasicTableOptionsWithNonCoveredRange;
 import static org.apache.kudu.test.ClientTestUtil.scanTableToStrings;
@@ -33,6 +34,8 @@ import java.util.ArrayList;
 import java.util.List;
 
 import com.google.common.collect.ImmutableList;
+import org.hamcrest.CoreMatchers;
+import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -348,6 +351,7 @@ public class TestKuduSession {
                               long successfulInserts,
                               long insertIgnoreErrors,
                               long successfulUpserts,
+                              long upsertIgnoreErrors,
                               long successfulUpdates,
                               long updateIgnoreErrors,
                               long successfulDeletes,
@@ -356,7 +360,7 @@ public class TestKuduSession {
     assertEquals(successfulInserts, metrics.getMetric("successful_inserts"));
     assertEquals(insertIgnoreErrors, metrics.getMetric("insert_ignore_errors"));
     assertEquals(successfulUpserts, metrics.getMetric("successful_upserts"));
-    // TODO(yingchun): should test upsert_ignore_errors
+    assertEquals(upsertIgnoreErrors, metrics.getMetric("upsert_ignore_errors"));
     assertEquals(successfulUpdates, metrics.getMetric("successful_updates"));
     assertEquals(updateIgnoreErrors, metrics.getMetric("update_ignore_errors"));
     assertEquals(successfulDeletes, metrics.getMetric("successful_deletes"));
@@ -377,7 +381,7 @@ public class TestKuduSession {
         "INT32 key=1, INT32 column1_i=1, INT32 column2_i=3, " +
             "STRING column3_s=a string, BOOL column4_b=true",
         rowStrings.get(0));
-    doVerifyMetrics(session, 0, 0, 1, 0, 0, 0, 0);
+    doVerifyMetrics(session, 0, 0, 1, 0, 0, 0, 0, 0);
 
     // Test an Upsert that acts as an Update.
     assertFalse(session.apply(createUpsert(table, 1, 2, false)).hasRowError());
@@ -386,7 +390,7 @@ public class TestKuduSession {
         "INT32 key=1, INT32 column1_i=2, INT32 column2_i=3, " +
             "STRING column3_s=a string, BOOL column4_b=true",
         rowStrings.get(0));
-    doVerifyMetrics(session, 0, 0, 2, 0, 0, 0, 0);
+    doVerifyMetrics(session, 0, 0, 2, 0, 0, 0, 0, 0);
   }
 
   @Test(timeout = 10000)
@@ -399,7 +403,7 @@ public class TestKuduSession {
     session.apply(createUpsert(table, 1, 1, false));
     session.apply(createInsertIgnore(table, 1));
     List<OperationResponse> results = session.flush();
-    doVerifyMetrics(session, 1, 1, 1, 0, 0, 0, 0);
+    doVerifyMetrics(session, 1, 1, 1, 0, 0, 0, 0, 0);
     for (OperationResponse result : results) {
       assertFalse(result.toString(), result.hasRowError());
     }
@@ -420,7 +424,7 @@ public class TestKuduSession {
     session.apply(createInsertIgnore(table, 1));
     session.apply(createInsert(table, 1));
     List<OperationResponse> results = session.flush();
-    doVerifyMetrics(session, 1, 0, 0, 0, 0, 0, 0);
+    doVerifyMetrics(session, 1, 0, 0, 0, 0, 0, 0, 0);
     assertFalse(results.get(0).toString(), results.get(0).hasRowError());
     assertTrue(results.get(1).toString(), results.get(1).hasRowError());
     assertTrue(results.get(1).getRowError().getErrorStatus().isAlreadyPresent());
@@ -444,7 +448,7 @@ public class TestKuduSession {
             "INT32 key=1, INT32 column1_i=2, INT32 column2_i=3, " +
                     "STRING column3_s=a string, BOOL column4_b=true",
             rowStrings.get(0));
-    doVerifyMetrics(session, 1, 0, 0, 0, 0, 0, 0);
+    doVerifyMetrics(session, 1, 0, 0, 0, 0, 0, 0, 0);
 
     // Test insert ignore does not return a row error.
     assertFalse(session.apply(createInsertIgnore(table, 1)).hasRowError());
@@ -453,7 +457,7 @@ public class TestKuduSession {
             "INT32 key=1, INT32 column1_i=2, INT32 column2_i=3, " +
                     "STRING column3_s=a string, BOOL column4_b=true",
             rowStrings.get(0));
-    doVerifyMetrics(session, 1, 1, 0, 0, 0, 0, 0);
+    doVerifyMetrics(session, 1, 1, 0, 0, 0, 0, 0, 0);
 
   }
 
@@ -465,11 +469,11 @@ public class TestKuduSession {
     // Test update ignore does not return a row error.
     assertFalse(session.apply(createUpdateIgnore(table, 1, 1, false)).hasRowError());
     assertEquals(0, scanTableToStrings(table).size());
-    doVerifyMetrics(session, 0, 0, 0, 0, 1, 0, 0);
+    doVerifyMetrics(session, 0, 0, 0, 0, 0, 1, 0, 0);
 
     assertFalse(session.apply(createInsert(table, 1)).hasRowError());
     assertEquals(1, scanTableToStrings(table).size());
-    doVerifyMetrics(session, 1, 0, 0, 0, 1, 0, 0);
+    doVerifyMetrics(session, 1, 0, 0, 0, 0, 1, 0, 0);
 
     // Test update ignore implements normal update.
     assertFalse(session.apply(createUpdateIgnore(table, 1, 2, false)).hasRowError());
@@ -479,7 +483,7 @@ public class TestKuduSession {
         "INT32 key=1, INT32 column1_i=2, INT32 column2_i=3, " +
             "STRING column3_s=a string, BOOL column4_b=true",
         rowStrings.get(0));
-    doVerifyMetrics(session, 1, 0, 0, 1, 1, 0, 0);
+    doVerifyMetrics(session, 1, 0, 0, 0, 1, 1, 0, 0);
   }
 
   @Test(timeout = 10000)
@@ -489,16 +493,16 @@ public class TestKuduSession {
 
     // Test delete ignore does not return a row error.
     assertFalse(session.apply(createDeleteIgnore(table, 1)).hasRowError());
-    doVerifyMetrics(session, 0, 0, 0, 0, 0, 0, 1);
+    doVerifyMetrics(session, 0, 0, 0, 0, 0, 0, 0, 1);
 
     assertFalse(session.apply(createInsert(table, 1)).hasRowError());
     assertEquals(1, scanTableToStrings(table).size());
-    doVerifyMetrics(session, 1, 0, 0, 0, 0, 0, 1);
+    doVerifyMetrics(session, 1, 0, 0, 0, 0, 0, 0, 1);
 
     // Test delete ignore implements normal delete.
     assertFalse(session.apply(createDeleteIgnore(table, 1)).hasRowError());
     assertEquals(0, scanTableToStrings(table).size());
-    doVerifyMetrics(session, 1, 0, 0, 0, 0, 1, 1);
+    doVerifyMetrics(session, 1, 0, 0, 0, 0, 0, 1, 1);
   }
 
   @Test(timeout = 10000)
@@ -660,10 +664,220 @@ public class TestKuduSession {
     }
   }
 
+  @Test(timeout = 10000)
+  public void testUpdateOnTableWithImmutableColumn() throws Exception {
+    // Create a table with an immutable column.
+    KuduTable table = client.createTable(
+            tableName, createSchemaWithImmutableColumns(), getBasicCreateTableOptions());
+    KuduSession session = client.newSession();
+
+    // Insert some data and verify it.
+    assertFalse(session.apply(createInsertOnTableWithImmutableColumn(table, 1)).hasRowError());
+    List<String> rowStrings = scanTableToStrings(table);
+    assertEquals(1, rowStrings.size());
+    assertEquals("INT32 key=1, INT32 column1_i=2, INT32 column2_i=3, " +
+                    "STRING column3_s=a string, BOOL column4_b=true, INT32 column5_i=6",
+            rowStrings.get(0));
+    // successfulInserts++
+    doVerifyMetrics(session, 1, 0, 0, 0, 0, 0, 0, 0);
+
+    // Test an Update can update row without immutable column set.
+    final String expectRow = "INT32 key=1, INT32 column1_i=3, INT32 column2_i=3, " +
+        "STRING column3_s=NULL, BOOL column4_b=true, INT32 column5_i=6";
+    assertFalse(session.apply(createUpdateOnTableWithImmutableColumn(
+            table, 1, false)).hasRowError());
+    rowStrings = scanTableToStrings(table);
+    assertEquals(expectRow, rowStrings.get(0));
+    // successfulUpdates++
+    doVerifyMetrics(session, 1, 0, 0, 0, 1, 0, 0, 0);
+
+    // Test an Update results in an error when attempting to update row having at least
+    // one column with the immutable attribute set.
+    OperationResponse resp = session.apply(createUpdateOnTableWithImmutableColumn(
+            table, 1, true));
+    assertTrue(resp.hasRowError());
+    assertTrue(resp.getRowError().getErrorStatus().isImmutable());
+    Assert.assertThat(resp.getRowError().getErrorStatus().toString(),
+            CoreMatchers.containsString("Immutable: UPDATE not allowed for " +
+                    "immutable column: column5_i INT32 NULLABLE IMMUTABLE"));
+
+    // nothing changed
+    rowStrings = scanTableToStrings(table);
+    assertEquals(expectRow, rowStrings.get(0));
+    doVerifyMetrics(session, 1, 0, 0, 0, 1, 0, 0, 0);
+  }
+
+  @Test(timeout = 10000)
+  public void testUpdateIgnoreOnTableWithImmutableColumn() throws Exception {
+    // Create a table with an immutable column.
+    KuduTable table = client.createTable(
+            tableName, createSchemaWithImmutableColumns(), getBasicCreateTableOptions());
+    KuduSession session = client.newSession();
+
+    // Insert some data and verify it.
+    assertFalse(session.apply(createInsertOnTableWithImmutableColumn(table, 1)).hasRowError());
+    List<String> rowStrings = scanTableToStrings(table);
+    assertEquals(1, rowStrings.size());
+    assertEquals("INT32 key=1, INT32 column1_i=2, INT32 column2_i=3, " +
+                    "STRING column3_s=a string, BOOL column4_b=true, INT32 column5_i=6",
+            rowStrings.get(0));
+    // successfulInserts++
+    doVerifyMetrics(session, 1, 0, 0, 0, 0, 0, 0, 0);
+
+    final String expectRow = "INT32 key=1, INT32 column1_i=3, INT32 column2_i=3, " +
+            "STRING column3_s=NULL, BOOL column4_b=true, INT32 column5_i=6";
+
+    // Test an UpdateIgnore can update a row without changing the immutable column cell,
+    // the error of updating the immutable column will be ignored.
+    assertFalse(session.apply(createUpdateIgnoreOnTableWithImmutableColumn(
+            table, 1, true)).hasRowError());
+    rowStrings = scanTableToStrings(table);
+    assertEquals(expectRow, rowStrings.get(0));
+    // successfulUpdates++, updateIgnoreErrors++
+    doVerifyMetrics(session, 1, 0, 0, 0, 1, 1, 0, 0);
+
+    // Test an UpdateIgnore only on immutable column. Note that this will result in
+    // a 'Invalid argument: No fields updated' error.
+    OperationResponse resp = session.apply(createUpdateIgnoreOnTableWithImmutableColumn(
+            table, 1, false));
+    assertTrue(resp.hasRowError());
+    assertTrue(resp.getRowError().getErrorStatus().isInvalidArgument());
+    Assert.assertThat(resp.getRowError().getErrorStatus().toString(),
+        CoreMatchers.containsString("Invalid argument: No fields updated, " +
+                "key is: (int32 key=<redacted>)"));
+
+    // nothing changed
+    rowStrings = scanTableToStrings(table);
+    assertEquals(expectRow, rowStrings.get(0));
+    doVerifyMetrics(session, 1, 0, 0, 0, 1, 1, 0, 0);
+  }
+
+  @Test(timeout = 10000)
+  public void testUpsertIgnoreOnTableWithImmutableColumn() throws Exception {
+    // Create a table with an immutable column.
+    KuduTable table = client.createTable(
+            tableName, createSchemaWithImmutableColumns(), getBasicCreateTableOptions());
+    KuduSession session = client.newSession();
+
+    // Insert some data and verify it.
+    assertFalse(session.apply(createUpsertIgnoreOnTableWithImmutableColumn(
+            table, 1, 2, true)).hasRowError());
+    List<String> rowStrings = scanTableToStrings(table);
+    assertEquals(1, rowStrings.size());
+    assertEquals("INT32 key=1, INT32 column1_i=2, INT32 column2_i=3, " +
+                    "STRING column3_s=NULL, BOOL column4_b=true, INT32 column5_i=4",
+            rowStrings.get(0));
+    // successfulUpserts++
+    doVerifyMetrics(session, 0, 0, 1, 0, 0, 0, 0, 0);
+
+    // Test an UpsertIgnore can update row without immutable column set.
+    assertFalse(session.apply(createUpsertIgnoreOnTableWithImmutableColumn(
+            table, 1, 3, false)).hasRowError());
+    rowStrings = scanTableToStrings(table);
+    assertEquals("INT32 key=1, INT32 column1_i=3, INT32 column2_i=3, " +
+            "STRING column3_s=NULL, BOOL column4_b=true, INT32 column5_i=4", rowStrings.get(0));
+    // successfulUpserts++
+    doVerifyMetrics(session, 0, 0, 2, 0, 0, 0, 0, 0);
+
+    // Test an UpsertIgnore can update row with immutable column set.
+    assertFalse(session.apply(createUpsertIgnoreOnTableWithImmutableColumn(
+            table, 1, 4, true)).hasRowError());
+    rowStrings = scanTableToStrings(table);
+    assertEquals("INT32 key=1, INT32 column1_i=4, INT32 column2_i=3, " +
+            "STRING column3_s=NULL, BOOL column4_b=true, INT32 column5_i=4", rowStrings.get(0));
+    // successfulUpserts++, upsertIgnoreErrors++
+    doVerifyMetrics(session, 0, 0, 3, 1, 0, 0, 0, 0);
+  }
+
+  @Test(timeout = 10000)
+  public void testUpsertOnTableWithImmutableColumn() throws Exception {
+    // Create a table with an immutable column.
+    KuduTable table = client.createTable(
+            tableName, createSchemaWithImmutableColumns(), getBasicCreateTableOptions());
+    KuduSession session = client.newSession();
+
+    final String expectRow = "INT32 key=1, INT32 column1_i=2, INT32 column2_i=3, " +
+        "STRING column3_s=NULL, BOOL column4_b=true, INT32 column5_i=4";
+    // Insert some data and verify it.
+    assertFalse(session.apply(createUpsertOnTableWithImmutableColumn(
+            table, 1, 2, true)).hasRowError());
+    List<String> rowStrings = scanTableToStrings(table);
+    assertEquals(1, rowStrings.size());
+    assertEquals(expectRow, rowStrings.get(0));
+    // successfulUpserts++
+    doVerifyMetrics(session, 0, 0, 1, 0, 0, 0, 0, 0);
+
+    // Test an Upsert attemp to update an immutable column, which will result an error.
+    OperationResponse resp = session.apply(createUpsertOnTableWithImmutableColumn(
+            table, 1, 3, true));
+    assertTrue(resp.hasRowError());
+    assertTrue(resp.getRowError().getErrorStatus().isImmutable());
+    Assert.assertThat(resp.getRowError().getErrorStatus().toString(),
+        CoreMatchers.containsString("Immutable: UPDATE not allowed for " +
+                "immutable column: column5_i INT32 NULLABLE IMMUTABLE"));
+
+    // nothing changed
+    rowStrings = scanTableToStrings(table);
+    assertEquals(expectRow, rowStrings.get(0));
+    doVerifyMetrics(session, 0, 0, 1, 0, 0, 0, 0, 0);
+  }
+
   private Insert createInsert(KuduTable table, int key) {
     return createBasicSchemaInsert(table, key);
   }
 
+  private Insert createInsertOnTableWithImmutableColumn(KuduTable table, int key) {
+    Insert insert = createBasicSchemaInsert(table, key);
+    insert.getRow().addInt(5, 6);
+    return insert;
+  }
+
+  private Update createUpdateOnTableWithImmutableColumn(KuduTable table, int key,
+                                                        boolean updateImmutableColumn) {
+    Update update = table.newUpdate();
+    populateUpdateRow(update.getRow(), key, key * 3, true);
+    if (updateImmutableColumn) {
+      update.getRow().addInt(5, 6);
+    }
+
+    return update;
+  }
+
+  private UpdateIgnore createUpdateIgnoreOnTableWithImmutableColumn(
+          KuduTable table, int key, boolean updateNonImmutableColumns) {
+    UpdateIgnore updateIgnore = table.newUpdateIgnore();
+    if (updateNonImmutableColumns) {
+      populateUpdateRow(updateIgnore.getRow(), key, key * 3, true);
+    } else {
+      updateIgnore.getRow().addInt(0, key);
+    }
+    updateIgnore.getRow().addInt(5, 6);
+
+    return updateIgnore;
+  }
+
+  private UpsertIgnore createUpsertIgnoreOnTableWithImmutableColumn(
+          KuduTable table, int key, int times, boolean updateImmutableColumn) {
+    UpsertIgnore upsertIgnore = table.newUpsertIgnore();
+    populateUpdateRow(upsertIgnore.getRow(), key, key * times, true);
+    if (updateImmutableColumn) {
+      upsertIgnore.getRow().addInt(5, key * times * 2);
+    }
+
+    return upsertIgnore;
+  }
+
+  private Upsert createUpsertOnTableWithImmutableColumn(
+          KuduTable table, int key, int times, boolean updateImmutableColumn) {
+    Upsert upsert = table.newUpsert();
+    populateUpdateRow(upsert.getRow(), key, key * times, true);
+    if (updateImmutableColumn) {
+      upsert.getRow().addInt(5, key * times * 2);
+    }
+
+    return upsert;
+  }
+
   private Upsert createUpsert(KuduTable table, int key, int secondVal, boolean hasNull) {
     Upsert upsert = table.newUpsert();
     populateUpdateRow(upsert.getRow(), key, secondVal, hasNull);
diff --git a/java/kudu-client/src/test/java/org/apache/kudu/client/TestKuduTable.java b/java/kudu-client/src/test/java/org/apache/kudu/client/TestKuduTable.java
index ef3656b82..72fb70c90 100644
--- a/java/kudu-client/src/test/java/org/apache/kudu/client/TestKuduTable.java
+++ b/java/kudu-client/src/test/java/org/apache/kudu/client/TestKuduTable.java
@@ -23,6 +23,7 @@ import static org.apache.kudu.client.KuduPredicate.ComparisonOp.GREATER_EQUAL;
 import static org.apache.kudu.client.KuduPredicate.ComparisonOp.LESS;
 import static org.apache.kudu.client.KuduPredicate.ComparisonOp.LESS_EQUAL;
 import static org.apache.kudu.test.ClientTestUtil.createBasicSchemaInsert;
+import static org.apache.kudu.test.ClientTestUtil.createSchemaWithImmutableColumns;
 import static org.apache.kudu.test.ClientTestUtil.getBasicCreateTableOptions;
 import static org.apache.kudu.test.ClientTestUtil.getBasicSchema;
 import static org.apache.kudu.test.ClientTestUtil.getBasicTableOptionsWithNonCoveredRange;
@@ -2487,4 +2488,76 @@ public class TestKuduTable {
     assertTrue(currentStatistics.getLiveRowCount() >= prevStatistics.getLiveRowCount());
     assertEquals(num, currentStatistics.getLiveRowCount());
   }
+
+  @Test(timeout = 100000)
+  @KuduTestHarness.MasterServerConfig(flags = {
+      "--master_support_immutable_column_attribute=false"
+  })
+  public void testCreateTableWithImmuColsWhenMasterNotSupport() throws Exception {
+    try {
+      CreateTableOptions builder = getBasicCreateTableOptions();
+      client.createTable(tableName, createSchemaWithImmutableColumns(), builder);
+      fail("shouldn't be able to create a table with immutable columns " +
+          "when server side doesn't support required IMMUTABLE_COLUMN_ATTRIBUTE feature");
+    } catch (KuduException ex) {
+      final String errmsg = ex.getMessage();
+      assertTrue(errmsg, ex.getStatus().isRemoteError());
+      assertTrue(errmsg, errmsg.matches(
+          ".* server sent error unsupported feature flags"));
+    }
+  }
+
+  @Test(timeout = 100000)
+  @KuduTestHarness.MasterServerConfig(flags = {
+      "--master_support_immutable_column_attribute=false"
+  })
+  public void testAlterTableAddImmuColsWhenMasterNotSupport() throws Exception {
+    CreateTableOptions builder = getBasicCreateTableOptions();
+    client.createTable(tableName, BASIC_SCHEMA, builder);
+    final ColumnSchema immu_col = new ColumnSchema.ColumnSchemaBuilder("immu_col", Type.INT32)
+        .nullable(true).immutable(true).build();
+    try {
+      client.alterTable(tableName, new AlterTableOptions().addColumn(immu_col));
+      fail("shouldn't be able to alter a table to add a column with immutable attribute " +
+          "when server side doesn't support required IMMUTABLE_COLUMN_ATTRIBUTE feature");
+    } catch (KuduException ex) {
+      final String errmsg = ex.getMessage();
+      assertTrue(errmsg, ex.getStatus().isRemoteError());
+      assertTrue(errmsg, errmsg.matches(
+          ".* server sent error unsupported feature flags"));
+    }
+  }
+
+  @Test(timeout = 100000)
+  @KuduTestHarness.MasterServerConfig(flags = {
+      "--master_support_immutable_column_attribute=false"
+  })
+  public void testAlterTableAlterImmuColsWhenMasterNotSupport() throws Exception {
+    CreateTableOptions builder = getBasicCreateTableOptions();
+    client.createTable(tableName, BASIC_SCHEMA, builder);
+    try {
+      client.alterTable(tableName, new AlterTableOptions().changeImmutable("column1_i", true));
+      fail("shouldn't be able to alter a table to change the immutable attribute on a column " +
+          "when server side doesn't support required IMMUTABLE_COLUMN_ATTRIBUTE feature");
+    } catch (KuduException ex) {
+      final String errmsg = ex.getMessage();
+      assertTrue(errmsg, ex.getStatus().isRemoteError());
+      assertTrue(errmsg, errmsg.matches(
+          ".* server sent error unsupported feature flags"));
+    }
+
+    // No matter if the table has an immutable attribute column or not, we can test the function
+    // on the client side, the request will be processed by the generic RPC code and throw an
+    // exception before reaching particular application code.
+    try {
+      client.alterTable(tableName, new AlterTableOptions().changeImmutable("column1_i", false));
+      fail("shouldn't be able to alter a table to change the immutable attribute on a column " +
+          "when server side doesn't support required IMMUTABLE_COLUMN_ATTRIBUTE feature");
+    } catch (KuduException ex) {
+      final String errmsg = ex.getMessage();
+      assertTrue(errmsg, ex.getStatus().isRemoteError());
+      assertTrue(errmsg, errmsg.matches(
+          ".* server sent error unsupported feature flags"));
+    }
+  }
 }
diff --git a/java/kudu-test-utils/src/main/java/org/apache/kudu/test/ClientTestUtil.java b/java/kudu-test-utils/src/main/java/org/apache/kudu/test/ClientTestUtil.java
index df6a59a1e..8cb7d28b8 100644
--- a/java/kudu-test-utils/src/main/java/org/apache/kudu/test/ClientTestUtil.java
+++ b/java/kudu-test-utils/src/main/java/org/apache/kudu/test/ClientTestUtil.java
@@ -490,4 +490,11 @@ public abstract class ClientTestUtil {
         ).build());
     return new Schema(columns);
   }
+
+  public static Schema createSchemaWithImmutableColumns() {
+    List<ColumnSchema> columns = new ArrayList<>(ClientTestUtil.getBasicSchema().getColumns());
+    columns.add(new ColumnSchema.ColumnSchemaBuilder("column5_i", Type.INT32)
+            .nullable(true).immutable(true).build());
+    return new Schema(columns);
+  }
 }