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 e8ce3a9bf KUDU-2671  Make WebUI compatible with custom hash schema
e8ce3a9bf is described below

commit e8ce3a9bff9d3a0c00af7e71592e552c33ce5b24
Author: Abhishek Chennaka <[email protected]>
AuthorDate: Fri Jul 8 01:59:58 2022 -0400

    KUDU-2671  Make WebUI compatible with custom hash schema
    
    This patch updates the /table?id=<table_id> page in the Kudu master
    WebUI to show custom hash schemas in the sections of:
    
    1. Partition Schema
    The custom hash schema if present for a particular range is displayed
    right beside the range schema. Different dimensions of the hash
    schema are comma separated.
    
    2. Detail
    There are new columns to identify if a particular partition has
    custom or table wide hash schema, display the hash schema and the hash
    partition id of the partition.
    
    The Kudu tablet server WebUI's pages /tablets and
    /tablet?id=<tablet_id> are also tested to reflect the custom hash
    schema or table wide hash schema accordingly.
    
    Below are the screenshots of the WebUI after the changes
    Master WebUI:
    https://i.imgur.com/O4ra4JA.png
    Tablet server WebUI:
    https://i.imgur.com/BxdfsYt.png
    https://i.imgur.com/l2wA08Q.png
    
    Change-Id: Ic8b8d90f70c39f13b838e858c870e08dacbdfcd3
    Reviewed-on: http://gerrit.cloudera.org:8080/18712
    Reviewed-by: Alexey Serbin <[email protected]>
    Tested-by: Kudu Jenkins
---
 src/kudu/client/flex_partitioning_client-test.cc | 108 +++++++++++++
 src/kudu/common/partition-test.cc                |  10 +-
 src/kudu/common/partition.cc                     | 101 +++++++++++-
 src/kudu/common/partition.h                      |   8 +
 src/kudu/master/master-test.cc                   | 196 ++++++++++++++++++++++-
 src/kudu/master/master_path_handlers.cc          |   6 +-
 6 files changed, 413 insertions(+), 16 deletions(-)

diff --git a/src/kudu/client/flex_partitioning_client-test.cc 
b/src/kudu/client/flex_partitioning_client-test.cc
index 8a56050d1..c8af1b26d 100644
--- a/src/kudu/client/flex_partitioning_client-test.cc
+++ b/src/kudu/client/flex_partitioning_client-test.cc
@@ -37,6 +37,7 @@
 #include "kudu/gutil/port.h"
 #include "kudu/gutil/ref_counted.h"
 #include "kudu/gutil/stl_util.h"
+#include "kudu/gutil/strings/substitute.h"
 #include "kudu/master/catalog_manager.h"
 #include "kudu/master/master.h"
 #include "kudu/master/mini_master.h"
@@ -47,6 +48,8 @@
 #include "kudu/tserver/tablet_server.h"
 #include "kudu/tserver/ts_tablet_manager.h"
 #include "kudu/util/metrics.h"
+#include "kudu/util/curl_util.h"
+#include "kudu/util/faststring.h"
 #include "kudu/util/net/sockaddr.h"
 #include "kudu/util/slice.h"
 #include "kudu/util/status.h"
@@ -55,6 +58,7 @@
 
 DECLARE_bool(enable_per_range_hash_schemas);
 DECLARE_int32(heartbeat_interval_ms);
+DECLARE_string(webserver_doc_root);
 
 METRIC_DECLARE_counter(scans_started);
 
@@ -63,9 +67,13 @@ using kudu::client::KuduValue;
 using kudu::cluster::InternalMiniCluster;
 using kudu::cluster::InternalMiniClusterOptions;
 using kudu::master::CatalogManager;
+using kudu::master::TableInfo;
+using kudu::master::TabletInfo;
+using kudu::tablet::TabletReplica;
 using std::string;
 using std::unique_ptr;
 using std::vector;
+using strings::Substitute;
 
 static constexpr const char* const kKeyColumn = "key";
 static constexpr const char* const kIntValColumn = "int_val";
@@ -97,6 +105,10 @@ class FlexPartitioningTest : public KuduTest {
     // Reduce the TS<->Master heartbeat interval to speed up testing.
     FLAGS_heartbeat_interval_ms = 10;
 
+    // Ensure the static pages are not available as tests are written based
+    // on this value of the flag
+    FLAGS_webserver_doc_root = "";
+
     // Start minicluster and wait for tablet servers to connect to master.
     cluster_.reset(new InternalMiniCluster(env_, 
InternalMiniClusterOptions()));
     ASSERT_OK(cluster_->Start());
@@ -546,6 +558,102 @@ TEST_F(FlexPartitioningCreateTableTest, 
DefaultAndCustomHashSchemas) {
   }
 }
 
+TEST_F(FlexPartitioningCreateTableTest, TabletServerWebUI) {
+  // Create a table with the following partitions:
+  //
+  //            hash bucket
+  //   key    0           1           2               3
+  //         -----------------------------------------------------------
+  //  <111    x:{key}     x:{key}     -               -
+  // 111-222  x:{key}     x:{key}     x:{key}         -
+  // 222-333  x:{key}     x:{key}     x:{key}     x:{key}
+  constexpr const char* const kTableName = "TabletServerWebUI";
+
+  unique_ptr<KuduTableCreator> table_creator(client_->NewTableCreator());
+  table_creator->table_name(kTableName)
+      .schema(&schema_)
+      .num_replicas(1)
+      .add_hash_partitions({ kKeyColumn }, 2)
+      .set_range_partition_columns({ kKeyColumn });
+
+  // Add a range partition with the table-wide hash partitioning rules.
+  {
+    unique_ptr<KuduPartialRow> lower(schema_.NewRow());
+    ASSERT_OK(lower->SetInt32(kKeyColumn, INT32_MIN));
+    unique_ptr<KuduPartialRow> upper(schema_.NewRow());
+    ASSERT_OK(upper->SetInt32(kKeyColumn, 111));
+    table_creator->add_range_partition(lower.release(), upper.release());
+  }
+
+  // Add a range partition with custom hash sub-partitioning rules:
+  // 3 buckets with hash based on the "key" column with hash seed 1.
+  {
+    auto p = CreateRangePartition(111, 222);
+    ASSERT_OK(p->add_hash_partitions({ kKeyColumn }, 3, 1));
+    table_creator->add_custom_range_partition(p.release());
+  }
+
+  // Add a range partition with custom hash sub-partitioning rules:
+  // 4 buckets with hash based on the "key" column with hash seed 2.
+  {
+    auto p = CreateRangePartition(222, 333);
+    ASSERT_OK(p->add_hash_partitions({ kKeyColumn }, 4, 2));
+    table_creator->add_custom_range_partition(p.release());
+  }
+
+  ASSERT_OK(table_creator->Create());
+  NO_FATALS(CheckTabletCount(kTableName, 9));
+
+  // Obtain the web page contents
+  EasyCurl c;
+  faststring buf;
+  ASSERT_OK(c.FetchURL(Substitute("http://$0/tablets";,
+                                  
cluster_->mini_tablet_server(0)->bound_http_addr().ToString()),
+                       &buf));
+  string raw = buf.ToString();
+
+  // Get the list of tablets present in this table
+  std::vector<scoped_refptr<TableInfo>> tables;
+  {
+    CatalogManager::ScopedLeaderSharedLock l(
+        cluster_->mini_master(0)->master()->catalog_manager());
+    
cluster_->mini_master(0)->master()->catalog_manager()->GetAllTables(&tables);
+  }
+  ASSERT_EQ(1, tables.size());
+  vector<scoped_refptr<TabletInfo>> tablets;
+  tables.front()->GetAllTablets(&tablets);
+
+  ASSERT_EQ(9, tablets.size());
+  // Validate the partition information rendered in the page
+  ASSERT_STR_CONTAINS(raw, Substitute("id=$0\"},\"partition\":\"HASH (key) 
PARTITION 0, "
+                                      "RANGE (key) PARTITION -2147483648 <= 
VALUES < 111",
+                                      tablets[0]->id()));
+  ASSERT_STR_CONTAINS(raw, Substitute("id=$0\"},\"partition\":\"HASH (key) 
PARTITION 0, "
+                                      "RANGE (key) PARTITION 111 <= VALUES < 
222",
+                                      tablets[1]->id()));
+  ASSERT_STR_CONTAINS(raw, Substitute("id=$0\"},\"partition\":\"HASH (key) 
PARTITION 0, "
+                                      "RANGE (key) PARTITION 222 <= VALUES < 
333",
+                                      tablets[2]->id()));
+  ASSERT_STR_CONTAINS(raw, Substitute("id=$0\"},\"partition\":\"HASH (key) 
PARTITION 1, "
+                                      "RANGE (key) PARTITION -2147483648 <= 
VALUES < 111",
+                                      tablets[3]->id()));
+  ASSERT_STR_CONTAINS(raw, Substitute("id=$0\"},\"partition\":\"HASH (key) 
PARTITION 1, "
+                                      "RANGE (key) PARTITION 111 <= VALUES < 
222",
+                                      tablets[4]->id()));
+  ASSERT_STR_CONTAINS(raw, Substitute("id=$0\"},\"partition\":\"HASH (key) 
PARTITION 1, "
+                                      "RANGE (key) PARTITION 222 <= VALUES < 
333",
+                                      tablets[5]->id()));
+  ASSERT_STR_CONTAINS(raw, Substitute("id=$0\"},\"partition\":\"HASH (key) 
PARTITION 2, "
+                                      "RANGE (key) PARTITION 111 <= VALUES < 
222",
+                                      tablets[6]->id()));
+  ASSERT_STR_CONTAINS(raw, Substitute("id=$0\"},\"partition\":\"HASH (key) 
PARTITION 2, "
+                                      "RANGE (key) PARTITION 222 <= VALUES < 
333",
+                                      tablets[7]->id()));
+  ASSERT_STR_CONTAINS(raw, Substitute("id=$0\"},\"partition\":\"HASH (key) 
PARTITION 3, "
+                                      "RANGE (key) PARTITION 222 <= VALUES < 
333",
+                                      tablets[8]->id()));
+}
+
 // Parameters for a single hash dimension.
 struct HashDimensionParameters {
   vector<string> columns; // names of the columns to use for hash bucketing
diff --git a/src/kudu/common/partition-test.cc 
b/src/kudu/common/partition-test.cc
index 1202f96ee..b992a3957 100644
--- a/src/kudu/common/partition-test.cc
+++ b/src/kudu/common/partition-test.cc
@@ -198,7 +198,7 @@ TEST_F(PartitionTest, TestCompoundRangeKeyEncoding) {
             partition_schema.PartitionDebugString(partitions[1], schema));
   EXPECT_EQ(R"(RANGE (c1, c2, c3) PARTITION ("", "b", "c") <= VALUES < ("d", 
"", "f"))",
             partition_schema.PartitionDebugString(partitions[2], schema));
-  EXPECT_EQ(R"(RANGE (c1, c2, c3) PARTITION VALUES >= ("e", "", ""))",
+  EXPECT_EQ(R"(RANGE (c1, c2, c3) PARTITION ("e", "", "") <= VALUES)",
             partition_schema.PartitionDebugString(partitions[3], schema));
 }
 
@@ -597,7 +597,7 @@ TEST_F(PartitionTest, TestCreatePartitions) {
   EXPECT_EQ(string("\0\0\0\0" "\0\0\0\0" "a2\0\0b2\0\0", 16), 
partitions[2].begin().ToString());
   EXPECT_EQ(string("\0\0\0\0" "\0\0\0\1", 8), partitions[2].end().ToString());
   EXPECT_EQ("HASH (a) PARTITION 0, HASH (b) PARTITION 0, "
-            R"(RANGE (a, b, c) PARTITION VALUES >= ("a2", "b2", ""))",
+            R"(RANGE (a, b, c) PARTITION ("a2", "b2", "") <= VALUES)",
             partition_schema.PartitionDebugString(partitions[2], schema));
 
   EXPECT_EQ(0, partitions[3].hash_buckets()[0]);
@@ -628,7 +628,7 @@ TEST_F(PartitionTest, TestCreatePartitions) {
   EXPECT_EQ(string("\0\0\0\0" "\0\0\0\1" "a2\0\0b2\0\0", 16), 
partitions[5].begin().ToString());
   EXPECT_EQ(string("\0\0\0\1", 4), partitions[5].end().ToString());
   EXPECT_EQ("HASH (a) PARTITION 0, HASH (b) PARTITION 1, "
-            R"(RANGE (a, b, c) PARTITION VALUES >= ("a2", "b2", ""))",
+            R"(RANGE (a, b, c) PARTITION ("a2", "b2", "") <= VALUES)",
             partition_schema.PartitionDebugString(partitions[5], schema));
 
   EXPECT_EQ(1, partitions[6].hash_buckets()[0]);
@@ -659,7 +659,7 @@ TEST_F(PartitionTest, TestCreatePartitions) {
   EXPECT_EQ(string("\0\0\0\1" "\0\0\0\0" "a2\0\0b2\0\0", 16), 
partitions[8].begin().ToString());
   EXPECT_EQ(string("\0\0\0\1" "\0\0\0\1", 8), partitions[8].end().ToString());
   EXPECT_EQ("HASH (a) PARTITION 1, HASH (b) PARTITION 0, "
-            R"(RANGE (a, b, c) PARTITION VALUES >= ("a2", "b2", ""))",
+            R"(RANGE (a, b, c) PARTITION ("a2", "b2", "") <= VALUES)",
             partition_schema.PartitionDebugString(partitions[8], schema));
 
   EXPECT_EQ(1, partitions[9].hash_buckets()[0]);
@@ -690,7 +690,7 @@ TEST_F(PartitionTest, TestCreatePartitions) {
   EXPECT_EQ(string("\0\0\0\1" "\0\0\0\1" "a2\0\0b2\0\0", 16), 
partitions[11].begin().ToString());
   EXPECT_EQ("", partitions[11].end().ToString());
   EXPECT_EQ("HASH (a) PARTITION 1, HASH (b) PARTITION 1, "
-            R"(RANGE (a, b, c) PARTITION VALUES >= ("a2", "b2", ""))",
+            R"(RANGE (a, b, c) PARTITION ("a2", "b2", "") <= VALUES)",
             partition_schema.PartitionDebugString(partitions[11], schema));
 }
 
diff --git a/src/kudu/common/partition.cc b/src/kudu/common/partition.cc
index 309071ef4..85bfbfb96 100644
--- a/src/kudu/common/partition.cc
+++ b/src/kudu/common/partition.cc
@@ -997,7 +997,7 @@ string PartitionSchema::RangePartitionDebugString(const 
KuduPartialRow& lower_bo
     return Substitute("VALUES < $0", RangeKeyDebugString(upper_bound));
   }
   if (upper_unbounded) {
-    return Substitute("VALUES >= $0", RangeKeyDebugString(lower_bound));
+    return Substitute("$0 <= VALUES", RangeKeyDebugString(lower_bound));
   }
   // TODO(dan): recognize when a simplified 'VALUE =' form can be used (see
   // org.apache.kudu.client.Partition#formatRangePartition).
@@ -1028,6 +1028,54 @@ string PartitionSchema::RangePartitionDebugString(Slice 
lower_bound,
   return RangePartitionDebugString(lower, upper);
 }
 
+string PartitionSchema::RangeWithCustomHashPartitionDebugString(Slice 
lower_bound,
+                                                                Slice 
upper_bound,
+                                                                const Schema& 
schema) const {
+  // Partitions are considered metadata, so don't redact them.
+  ScopedDisableRedaction no_redaction;
+  HashSchema hash_schema = GetHashSchemaForRange(lower_bound.ToString());
+  KuduPartialRow lower(&schema);
+  Arena arena(256);
+
+  // Decode the lower and upper bounds
+  Status s = DecodeRangeKey(&lower_bound, &lower, &arena);
+  if (!s.ok()) {
+    return Substitute("<range-key-decode-error: $0>", s.ToString());
+  }
+
+  KuduPartialRow upper(&schema);
+  s = DecodeRangeKey(&upper_bound, &upper, &arena);
+  if (!s.ok()) {
+    return Substitute("<range-key-decode-error: $0>", s.ToString());
+  }
+
+  // Get the range bounds information for the partition
+  bool lower_unbounded = IsRangePartitionKeyEmpty(lower);
+  bool upper_unbounded = IsRangePartitionKeyEmpty(upper);
+  string partition;
+  if (lower_unbounded && upper_unbounded) {
+    partition = "UNBOUNDED";
+  } else if (lower_unbounded) {
+    partition = Substitute("VALUES < $0", RangeKeyDebugString(upper));
+  } else if (upper_unbounded) {
+    partition = Substitute("$0 <= VALUES", RangeKeyDebugString(lower));
+  } else {
+    partition = Substitute("$0 <= VALUES < $1",
+                           RangeKeyDebugString(lower),
+                           RangeKeyDebugString(upper));
+  }
+
+  // Get the hash schema information for the partition
+  if (hash_schema != hash_schema_) {
+    for (const auto& hash_dimension: hash_schema) {
+      partition.append(Substitute(" HASH($0) PARTITIONS $1",
+                                  ColumnIdsToColumnNames(schema, 
hash_dimension.column_ids),
+                                  hash_dimension.num_buckets));
+    }
+  }
+  return partition;
+}
+
 string PartitionSchema::RangeKeyDebugString(Slice range_key, const Schema& 
schema) const {
   Arena arena(256);
   KuduPartialRow row(&schema);
@@ -1124,11 +1172,12 @@ string PartitionSchema::DisplayString(const Schema& 
schema,
 
 string PartitionSchema::PartitionTableHeader(const Schema& schema) const {
   string header;
-  for (const auto& hash_schema : hash_schema_) {
-    SubstituteAndAppend(&header, "<th>HASH ($0) PARTITION</th>",
-                        EscapeForHtmlToString(ColumnIdsToColumnNames(
-                            schema, hash_schema.column_ids)));
+  if (!hash_schema_.empty()) {
+    header.append("<th>Hash Schema Type</th>");
+    header.append("<th>Hash Schema</th>");
+    header.append("<th>Hash Partition</th>");
   }
+
   if (!range_schema_.column_ids.empty()) {
     SubstituteAndAppend(&header, "<th>RANGE ($0) PARTITION</th>",
                         EscapeForHtmlToString(
@@ -1140,8 +1189,46 @@ string PartitionSchema::PartitionTableHeader(const 
Schema& schema) const {
 string PartitionSchema::PartitionTableEntry(const Schema& schema,
                                             const Partition& partition) const {
   string entry;
-  for (int32_t bucket : partition.hash_buckets_) {
-    SubstituteAndAppend(&entry, "<td>$0</td>", bucket);
+  const auto* idx_ptr = FindOrNull(
+      hash_schema_idx_by_encoded_range_start_, partition.begin_.range_key());
+
+  if (idx_ptr) {
+    const auto& range = ranges_with_custom_hash_schemas_[*idx_ptr];
+    entry.append("<td>Range Specific</td>");
+    entry.append("<td>");
+    for (size_t i = 0; i < range.hash_schema.size(); i++) {
+      SubstituteAndAppend(&entry, "HASH ($0) PARTITIONS $1<br>",
+                          EscapeForHtmlToString(ColumnIdsToColumnNames(
+                              schema, range.hash_schema[i].column_ids)),
+                          range.hash_schema[i].num_buckets);
+    }
+    entry.append("</td>");
+    entry.append("<td>");
+    for (size_t i = 0; i < range.hash_schema.size(); i++) {
+      SubstituteAndAppend(&entry, "HASH ($0) PARTITION: $1<br>",
+                          EscapeForHtmlToString(ColumnIdsToColumnNames(
+                              schema, range.hash_schema[i].column_ids)),
+                          partition.hash_buckets_[i]);
+    }
+    entry.append("</td>");
+  } else {
+    entry.append("<td>Table Wide</td>");
+    entry.append("<td>");
+    for (size_t i = 0; i < hash_schema_.size(); i++) {
+        SubstituteAndAppend(&entry, "HASH ($0) PARTITIONS $1<br>",
+                            EscapeForHtmlToString(ColumnIdsToColumnNames(
+                                schema, hash_schema_[i].column_ids)),
+                                hash_schema_[i].num_buckets);
+    }
+    entry.append("</td>");
+    entry.append("<td>");
+    for (size_t i = 0; i < hash_schema_.size(); i++) {
+      SubstituteAndAppend(&entry, "HASH ($0) PARTITION: $1<br>",
+                          EscapeForHtmlToString(ColumnIdsToColumnNames(
+                              schema, hash_schema_[i].column_ids)),
+                          partition.hash_buckets_[i]);
+    }
+    entry.append("</td>");
   }
 
   if (!range_schema_.column_ids.empty()) {
diff --git a/src/kudu/common/partition.h b/src/kudu/common/partition.h
index ef4b48d3a..bcfb1f2d1 100644
--- a/src/kudu/common/partition.h
+++ b/src/kudu/common/partition.h
@@ -401,6 +401,14 @@ class PartitionSchema {
                                         Slice upper_bound,
                                         const Schema& schema) const;
 
+  // Returns a text description of the partition with the provided inclusive
+  // lower bound and exclusive upper bound along with custom hash schema for
+  // the partition if present. The custom hash schema is a space separated
+  // descriptions of hash dimensions.
+  std::string RangeWithCustomHashPartitionDebugString(Slice lower_bound,
+                                                      Slice upper_bound,
+                                                      const Schema& schema) 
const;
+
   // Returns a text description of this partition schema suitable for debug 
printing.
   //
   // The partition schema is considered metadata, so partition bound 
information
diff --git a/src/kudu/master/master-test.cc b/src/kudu/master/master-test.cc
index d6bdda02c..b82bbb1e0 100644
--- a/src/kudu/master/master-test.cc
+++ b/src/kudu/master/master-test.cc
@@ -157,6 +157,10 @@ class MasterTest : public KuduTest {
     // but we have no tablet servers. Typically this would be disallowed.
     FLAGS_catalog_manager_check_ts_count_for_create_table = false;
 
+    // Ensure the static pages are not available as tests are written based
+    // on this value of the flag
+    FLAGS_webserver_doc_root = "";
+
     // Start master
     mini_master_.reset(new MiniMaster(GetTestPath("Master"), 
HostPort("127.0.0.1", 0)));
     ASSERT_OK(mini_master_->Start());
@@ -956,7 +960,7 @@ TEST_P(AlterTableWithRangeSpecificHashSchema, 
TestAlterTableWithDifferentHashDim
     custom_range_hash_schema = {{{"key"}, 3, 0}};
   }
 
-  //Create AlterTableRequestPB and populate it for the alter table operation
+  // Create AlterTableRequestPB and populate it for the alter table operation
   AlterTableRequestPB req;
   AlterTableResponsePB resp;
   RpcController controller;
@@ -1436,6 +1440,196 @@ TEST_F(MasterTest, 
AlterTableAddDropRangeWithTableWideHashSchema) {
   }
 }
 
+TEST_F(MasterTest, MasterWebUIWithCustomHashPartitioning) {
+  constexpr const char* const kTableName = "master_webui_custom_hash_ps";
+  constexpr const char* const kCol0 = "c_int32";
+  constexpr const char* const kCol1 = "c_int64";
+  const Schema kTableSchema({ColumnSchema(kCol0, INT32),
+                             ColumnSchema(kCol1, INT64)}, 2);
+  FLAGS_default_num_replicas = 1;
+
+  // Create a table with one range partition based on the table-wide hash 
schema.
+  CreateTableResponsePB create_table_resp;
+  {
+    KuduPartialRow lower(&kTableSchema);
+    ASSERT_OK(lower.SetInt32(kCol0, 0));
+    ASSERT_OK(lower.SetInt64(kCol1, 0));
+    KuduPartialRow upper(&kTableSchema);
+    ASSERT_OK(upper.SetInt32(kCol0, 100));
+    ASSERT_OK(upper.SetInt64(kCol1, 100));
+    ASSERT_OK(CreateTable(
+        kTableName, kTableSchema, nullopt, nullopt, nullopt, {}, {{lower, 
upper}},
+        {}, {{{kCol0}, 2, 0}, {{kCol1}, 2, 0}}, &create_table_resp));
+  }
+
+  // Get all the tablets of this table
+  std::vector<scoped_refptr<TableInfo>> tables;
+  {
+    CatalogManager::ScopedLeaderSharedLock l(master_->catalog_manager());
+    master_->catalog_manager()->GetAllTables(&tables);
+  }
+  ASSERT_EQ(1, tables.size());
+
+  vector<scoped_refptr<TabletInfo>> tablets;
+  tables.front()->GetAllTablets(&tablets);
+  ASSERT_EQ(4, tablets.size());
+  EasyCurl c;
+  faststring buf;
+  ASSERT_OK(c.FetchURL(Substitute("http://$0/table?id=$1";,
+                                  mini_master_->bound_http_addr().ToString(),
+                                  create_table_resp.table_id()),
+                       &buf));
+  string raw = buf.ToString();
+
+  // Check the "Partition Schema" section
+  ASSERT_STR_CONTAINS(raw,
+                      "\"partition_schema\":\""
+                      "HASH (c_int32) PARTITIONS 2,\\n"
+                      "HASH (c_int64) PARTITIONS 2,\\n"
+                      "RANGE (c_int32, c_int64) (\\n"
+                      "    PARTITION (0, 0) <= VALUES < (100, 100)\\n"
+                      ")\"");
+
+  // Check the "Detail" table section
+  {
+    int k = 0;
+    for (int i = 0; i < 2; i++) {
+      for (int j = 0; j < 2; j++) {
+        ASSERT_STR_CONTAINS(raw,
+                            Substitute(
+                                "\"id\":\"$0\",\"partition_cols\":\"<td>Table 
Wide</td>"
+                                "<td>HASH (c_int32) PARTITIONS 2<br>"
+                                "HASH (c_int64) PARTITIONS 2<br></td>"
+                                "<td>HASH (c_int32) PARTITION: $1<br>"
+                                "HASH (c_int64) PARTITION: $2<br></td>"
+                                "<td>(0, 0) &lt;= VALUES &lt; (100, 
100)</td>\"",
+                                tablets[k++]->id(), i, j));
+      }
+    }
+  }
+
+  const auto& table_id = create_table_resp.table_id();
+  const HashSchema custom_hash_schema{{{kCol0}, 2, 0}, {{kCol1}, 3, 0}};
+
+  // Alter the table, adding a new range with custom hash schema.
+  {
+    AlterTableRequestPB req;
+    AlterTableResponsePB resp;
+    req.mutable_table()->set_table_name(kTableName);
+    req.mutable_table()->set_table_id(table_id);
+
+    // Add the required information on the table's schema:
+    // key and non-null columns must be present in the request.
+    {
+      ColumnSchemaPB* col0 = req.mutable_schema()->add_columns();
+      col0->set_name(kCol0);
+      col0->set_type(INT32);
+      col0->set_is_key(true);
+
+      ColumnSchemaPB* col1 = req.mutable_schema()->add_columns();
+      col1->set_name(kCol1);
+      col1->set_type(INT64);
+      col1->set_is_key(true);
+    }
+
+    AlterTableRequestPB::Step* step = req.add_alter_schema_steps();
+    step->set_type(AlterTableRequestPB::ADD_RANGE_PARTITION);
+    KuduPartialRow lower(&kTableSchema);
+    ASSERT_OK(lower.SetInt32(kCol0, 100));
+    ASSERT_OK(lower.SetInt64(kCol1, 100));
+    KuduPartialRow upper(&kTableSchema);
+    ASSERT_OK(upper.SetInt32(kCol0, 200));
+    ASSERT_OK(upper.SetInt64(kCol1, 200));
+    RowOperationsPBEncoder enc(
+        step->mutable_add_range_partition()->mutable_range_bounds());
+    enc.Add(RowOperationsPB::RANGE_LOWER_BOUND, lower);
+    enc.Add(RowOperationsPB::RANGE_UPPER_BOUND, upper);
+    for (const auto& hash_dimension: custom_hash_schema) {
+      auto* hash_dimension_pb = step->mutable_add_range_partition()->
+          mutable_custom_hash_schema()->add_hash_schema();
+      for (const auto& col_name: hash_dimension.columns) {
+        hash_dimension_pb->add_columns()->set_name(col_name);
+      }
+      hash_dimension_pb->set_num_buckets(hash_dimension.num_buckets);
+      hash_dimension_pb->set_seed(hash_dimension.seed);
+    }
+
+    RpcController ctl;
+    ASSERT_OK(proxy_->AlterTable(req, &resp, &ctl));
+    ASSERT_FALSE(resp.has_error())
+                  << StatusFromPB(resp.error().status()).ToString();
+  }
+
+  ASSERT_OK(c.FetchURL(Substitute("http://$0/table?id=$1";,
+                                  mini_master_->bound_http_addr().ToString(),
+                                  create_table_resp.table_id()),
+                       &buf));
+  raw = buf.ToString();
+
+  // Check the "Partition Schema" section
+  ASSERT_STR_CONTAINS(raw, "\"partition_schema\":\""
+                           "HASH (c_int32) PARTITIONS 2,\\n"
+                           "HASH (c_int64) PARTITIONS 2,\\n"
+                           "RANGE (c_int32, c_int64) (\\n"
+                           "    PARTITION (0, 0) <= VALUES < (100, 100),\\n"
+                           "    PARTITION (100, 100) <= VALUES < (200, 200) "
+                           "HASH(c_int32) PARTITIONS 2 HASH(c_int64) 
PARTITIONS 3\\n)");
+
+  {
+    CatalogManager::ScopedLeaderSharedLock l(master_->catalog_manager());
+    master_->catalog_manager()->GetAllTables(&tables);
+  }
+  ASSERT_EQ(1, tables.size());
+
+  tables.front()->GetAllTablets(&tablets);
+  // At this point we have the previously created 4 tables and now added 6 
tablets
+  ASSERT_EQ(10, tablets.size());
+
+  // Check the "Detail" table section of all the 10 tablets present
+  for (int i = 0; i < 2; i++) {
+    ASSERT_STR_CONTAINS(raw,Substitute(
+        "\"id\":\"$0\",\"partition_cols\":\"<td>Table Wide</td>"
+        "<td>HASH (c_int32) PARTITIONS 2<br>"
+        "HASH (c_int64) PARTITIONS 2<br></td>"
+        "<td>HASH (c_int32) PARTITION: $1<br>"
+        "HASH (c_int64) PARTITION: 0<br></td>"
+        "<td>(0, 0) &lt;= VALUES &lt; (100, 100)</td>\"",
+        tablets[i*5+0]->id(), i));
+    ASSERT_STR_CONTAINS(raw,Substitute(
+        "\"id\":\"$0\",\"partition_cols\":\"<td>Range Specific</td>"
+        "<td>HASH (c_int32) PARTITIONS 2<br>"
+        "HASH (c_int64) PARTITIONS 3<br></td>"
+        "<td>HASH (c_int32) PARTITION: $1<br>"
+        "HASH (c_int64) PARTITION: 0<br></td>"
+        "<td>(100, 100) &lt;= VALUES &lt; (200, 200)</td>\"",
+        tablets[i*5+1]->id(), i));
+    ASSERT_STR_CONTAINS(raw,Substitute(
+        "\"id\":\"$0\",\"partition_cols\":\"<td>Table Wide</td>"
+        "<td>HASH (c_int32) PARTITIONS 2<br>"
+        "HASH (c_int64) PARTITIONS 2<br></td>"
+        "<td>HASH (c_int32) PARTITION: $1<br>"
+        "HASH (c_int64) PARTITION: 1<br></td>"
+        "<td>(0, 0) &lt;= VALUES &lt; (100, 100)</td>\"",
+        tablets[i*5+2]->id(), i));
+    ASSERT_STR_CONTAINS(raw,Substitute(
+        "\"id\":\"$0\",\"partition_cols\":\"<td>Range Specific</td>"
+        "<td>HASH (c_int32) PARTITIONS 2<br>"
+        "HASH (c_int64) PARTITIONS 3<br></td>"
+        "<td>HASH (c_int32) PARTITION: $1<br>"
+        "HASH (c_int64) PARTITION: 1<br></td>"
+        "<td>(100, 100) &lt;= VALUES &lt; (200, 200)</td>\"",
+        tablets[i*5+3]->id(), i));
+    ASSERT_STR_CONTAINS(raw,Substitute(
+        "\"id\":\"$0\",\"partition_cols\":\"<td>Range Specific</td>"
+        "<td>HASH (c_int32) PARTITIONS 2<br>"
+        "HASH (c_int64) PARTITIONS 3<br></td>"
+        "<td>HASH (c_int32) PARTITION: $1<br>"
+        "HASH (c_int64) PARTITION: 2<br></td>"
+        "<td>(100, 100) &lt;= VALUES &lt; (200, 200)</td>\"",
+        tablets[i*5+4]->id(), i));
+  }
+}
+
 TEST_F(MasterTest, TestCreateTableCheckRangeInvariants) {
   constexpr const char* const kTableName = "testtb";
   const Schema kTableSchema({ ColumnSchema("key", INT32), ColumnSchema("val", 
INT32) }, 1);
diff --git a/src/kudu/master/master_path_handlers.cc 
b/src/kudu/master/master_path_handlers.cc
index 36ff3b174..fbff516d7 100644
--- a/src/kudu/master/master_path_handlers.cc
+++ b/src/kudu/master/master_path_handlers.cc
@@ -472,9 +472,9 @@ void MasterPathHandlers::HandleTablePage(const 
Webserver::WebRequest& req,
                     partition.hash_buckets().end(),
                     [] (const int32_t& bucket) { return bucket == 0; })) {
       range_partitions.emplace_back(
-          
partition_schema.RangePartitionDebugString(partition.begin().range_key(),
-                                                     
partition.end().range_key(),
-                                                     schema));
+          
partition_schema.RangeWithCustomHashPartitionDebugString(partition.begin().range_key(),
+                                                                   
partition.end().range_key(),
+                                                                   schema));
     }
 
     // Combine the tablet details and partition info for each tablet.

Reply via email to