Repository: kudu
Updated Branches:
  refs/heads/master 22067edb4 -> 6be3f328a


Add KuduTable.getFormattedRangePartitions method

This adds an Impala-specific, unstable API to the KuduTable class for
retrieving the list of the range partitions in a table. The partitions
are formatted according to the proposed Impala DDL syntax for creating
tables.

Change-Id: Ia9b263d2444314d46533191918833840e75b7ba7
Reviewed-on: http://gerrit.cloudera.org:8080/4934
Reviewed-by: Jean-Daniel Cryans <[email protected]>
Tested-by: Kudu Jenkins


Project: http://git-wip-us.apache.org/repos/asf/kudu/repo
Commit: http://git-wip-us.apache.org/repos/asf/kudu/commit/6be3f328
Tree: http://git-wip-us.apache.org/repos/asf/kudu/tree/6be3f328
Diff: http://git-wip-us.apache.org/repos/asf/kudu/diff/6be3f328

Branch: refs/heads/master
Commit: 6be3f328a07c1748c4928e87e28b62e6b07c1e4d
Parents: 22067ed
Author: Dan Burkert <[email protected]>
Authored: Wed Nov 2 13:02:39 2016 -0700
Committer: Dan Burkert <[email protected]>
Committed: Wed Nov 16 21:54:12 2016 +0000

----------------------------------------------------------------------
 .../java/org/apache/kudu/client/KeyEncoder.java |  35 ++-
 .../java/org/apache/kudu/client/KuduTable.java  |  27 ++
 .../java/org/apache/kudu/client/PartialRow.java | 296 ++++++++++++++++---
 .../java/org/apache/kudu/client/Partition.java  |  85 ++++++
 .../java/org/apache/kudu/client/RowResult.java  |   2 +-
 .../java/org/apache/kudu/util/StringUtil.java   |  90 ++++++
 .../org/apache/kudu/client/TestKuduTable.java   | 171 ++++++++++-
 .../org/apache/kudu/client/TestOperation.java   |   2 +-
 .../org/apache/kudu/util/TestStringUtil.java    |  40 +++
 9 files changed, 690 insertions(+), 58 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/kudu/blob/6be3f328/java/kudu-client/src/main/java/org/apache/kudu/client/KeyEncoder.java
----------------------------------------------------------------------
diff --git 
a/java/kudu-client/src/main/java/org/apache/kudu/client/KeyEncoder.java 
b/java/kudu-client/src/main/java/org/apache/kudu/client/KeyEncoder.java
index c898e5f..08fd9a2 100644
--- a/java/kudu-client/src/main/java/org/apache/kudu/client/KeyEncoder.java
+++ b/java/kudu-client/src/main/java/org/apache/kudu/client/KeyEncoder.java
@@ -247,7 +247,6 @@ class KeyEncoder {
     buf.order(ByteOrder.BIG_ENDIAN);
 
     List<Integer> buckets = new ArrayList<>();
-    PartialRow row = schema.newPartialRow();
 
     for (HashBucketSchema hashSchema : partitionSchema.getHashBucketSchemas()) 
{
       if (buf.hasRemaining()) {
@@ -257,6 +256,37 @@ class KeyEncoder {
       }
     }
 
+    return new Pair<>(buckets, decodeRangePartitionKey(schema, 
partitionSchema, buf));
+  }
+
+  /**
+   * Decodes a range partition key into a partial row.
+   *
+   * @param schema the schema of the table
+   * @param partitionSchema the partition schema of the table
+   * @param key the encoded range partition key
+   * @return the decoded range key
+   */
+  public static PartialRow decodeRangePartitionKey(Schema schema,
+                                                   PartitionSchema 
partitionSchema,
+                                                   byte[] key) {
+    ByteBuffer buf = ByteBuffer.wrap(key);
+    buf.order(ByteOrder.BIG_ENDIAN);
+    return decodeRangePartitionKey(schema, partitionSchema, buf);
+  }
+
+  /**
+   * Decodes a range partition key into a partial row.
+   *
+   * @param schema the schema of the table
+   * @param partitionSchema the partition schema of the table
+   * @param buf the encoded range partition key
+   * @return the decoded range key
+   */
+  private static PartialRow decodeRangePartitionKey(Schema schema,
+                                                    PartitionSchema 
partitionSchema,
+                                                    ByteBuffer buf) {
+    PartialRow row = schema.newPartialRow();
     Iterator<Integer> rangeIds = 
partitionSchema.getRangeSchema().getColumns().iterator();
     while (rangeIds.hasNext()) {
       int idx = schema.getColumnIndex(rangeIds.next());
@@ -270,8 +300,7 @@ class KeyEncoder {
     if (buf.hasRemaining()) {
       throw new IllegalArgumentException("Unable to decode all partition key 
bytes");
     }
-
-    return new Pair<>(buckets, row);
+    return row;
   }
 
   /**

http://git-wip-us.apache.org/repos/asf/kudu/blob/6be3f328/java/kudu-client/src/main/java/org/apache/kudu/client/KuduTable.java
----------------------------------------------------------------------
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 0bc9a35..b990369 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
@@ -17,8 +17,11 @@
 
 package org.apache.kudu.client;
 
+import java.util.ArrayList;
 import java.util.List;
 
+import com.google.common.base.Predicates;
+import com.google.common.collect.Iterators;
 import com.stumbleupon.async.Deferred;
 
 import org.apache.kudu.Schema;
@@ -201,4 +204,28 @@ public class KuduTable {
                                                  long deadline) throws 
Exception {
     return client.syncLocateTable(this, startKey, endKey, deadline);
   }
+
+  /**
+   * Retrieves a formatted representation of this table's range partitions. The
+   * range partitions will be returned in sorted order by value, and will
+   * contain no duplicates.
+   *
+   * @param deadline the deadline of the operation
+   * @return a list of the formatted range partitions
+   */
+  @InterfaceAudience.LimitedPrivate("Impala")
+  @InterfaceStability.Unstable
+  public List<String> getFormattedRangePartitions(long deadline) throws 
Exception {
+    List<String> rangePartitions = new ArrayList<>();
+    for (LocatedTablet tablet : getTabletsLocations(deadline)) {
+      Partition partition = tablet.getPartition();
+      // Filter duplicate range partitions by taking only the tablets whose 
hash
+      // partitions are all 0s.
+      if (!Iterators.all(partition.getHashBuckets().iterator(), 
Predicates.equalTo(0))) {
+        continue;
+      }
+      rangePartitions.add(partition.formatRangePartition(this));
+    }
+    return rangePartitions;
+  }
 }

http://git-wip-us.apache.org/repos/asf/kudu/blob/6be3f328/java/kudu-client/src/main/java/org/apache/kudu/client/PartialRow.java
----------------------------------------------------------------------
diff --git 
a/java/kudu-client/src/main/java/org/apache/kudu/client/PartialRow.java 
b/java/kudu-client/src/main/java/org/apache/kudu/client/PartialRow.java
index 21142d2..82250cf 100644
--- a/java/kudu-client/src/main/java/org/apache/kudu/client/PartialRow.java
+++ b/java/kudu-client/src/main/java/org/apache/kudu/client/PartialRow.java
@@ -22,6 +22,7 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.BitSet;
 import java.util.List;
+import java.util.ListIterator;
 
 import com.google.common.base.Preconditions;
 import com.google.common.collect.Lists;
@@ -31,6 +32,7 @@ import org.apache.kudu.Schema;
 import org.apache.kudu.Type;
 import org.apache.kudu.annotations.InterfaceAudience;
 import org.apache.kudu.annotations.InterfaceStability;
+import org.apache.kudu.util.StringUtil;
 
 /**
  * Class used to represent parts of a row along with its schema.<p>
@@ -541,7 +543,7 @@ public class PartialRow {
   /**
    * Appends a debug string for the provided columns in the row.
    *
-   * @param idxs the column indexes.
+   * @param idxs the column indexes
    * @param sb the string builder to append to
    */
   void appendDebugString(List<Integer> idxs, StringBuilder sb) {
@@ -560,54 +562,85 @@ public class PartialRow {
       sb.append(col.getName());
       sb.append('=');
 
-      Preconditions.checkState(columnsBitSet.get(idx), "Column %s is not set", 
col.getName());
+      appendCellValueDebugString(idx, sb);
+    }
+  }
 
-      if (nullsBitSet != null && nullsBitSet.get(idx)) {
-        sb.append("NULL");
-        continue;
+  /**
+   * Appends a short debug string for the provided columns in the row.
+   *
+   * @param idxs the column indexes
+   * @param sb the string builder to append to
+   */
+  void appendShortDebugString(List<Integer> idxs, StringBuilder sb) {
+    boolean first = true;
+    for (int idx : idxs) {
+      if (first) {
+        first = false;
+      } else {
+        sb.append(", ");
       }
+      appendCellValueDebugString(idx, sb);
+    }
+  }
 
-      switch (col.getType()) {
-        case BOOL:
-          sb.append(Bytes.getBoolean(rowAlloc, schema.getColumnOffset(idx)));
-          break;
-        case INT8:
-          sb.append(Bytes.getByte(rowAlloc, schema.getColumnOffset(idx)));
-          break;
-        case INT16:
-          sb.append(Bytes.getShort(rowAlloc, schema.getColumnOffset(idx)));
-          break;
-        case INT32:
-          sb.append(Bytes.getInt(rowAlloc, schema.getColumnOffset(idx)));
-          break;
-        case INT64:
-          sb.append(Bytes.getLong(rowAlloc, schema.getColumnOffset(idx)));
-          break;
-        case UNIXTIME_MICROS:
-          sb.append(RowResult.timestampToString(
-              Bytes.getLong(rowAlloc, schema.getColumnOffset(idx))));
-          break;
-        case FLOAT:
-          sb.append(Bytes.getFloat(rowAlloc, schema.getColumnOffset(idx)));
-          break;
-        case DOUBLE:
-          sb.append(Bytes.getDouble(rowAlloc, schema.getColumnOffset(idx)));
-          break;
-        case BINARY:
-        case STRING:
-          ByteBuffer value = getVarLengthData().get(idx).duplicate();
-          value.reset(); // Make sure we start at the beginning.
-          byte[] data = new byte[value.limit()];
-          value.get(data);
-          if (col.getType() == Type.STRING) {
-            sb.append(Bytes.getString(data));
-          } else {
-            sb.append(Bytes.pretty(data));
-          }
-          break;
-        default:
-          throw new RuntimeException("unreachable");
-      }
+  /**
+   * Appends a debug string for the provided cell value in the row.
+   *
+   * @param idx the column index
+   * @param sb the string builder to append to
+   */
+  void appendCellValueDebugString(Integer idx, StringBuilder sb) {
+    ColumnSchema col = schema.getColumnByIndex(idx);
+    Preconditions.checkState(columnsBitSet.get(idx), "Column %s is not set", 
col.getName());
+
+    if (nullsBitSet != null && nullsBitSet.get(idx)) {
+      sb.append("NULL");
+      return;
+    }
+
+    switch (col.getType()) {
+      case BOOL:
+        sb.append(Bytes.getBoolean(rowAlloc, schema.getColumnOffset(idx)));
+        return;
+      case INT8:
+        sb.append(Bytes.getByte(rowAlloc, schema.getColumnOffset(idx)));
+        return;
+      case INT16:
+        sb.append(Bytes.getShort(rowAlloc, schema.getColumnOffset(idx)));
+        return;
+      case INT32:
+        sb.append(Bytes.getInt(rowAlloc, schema.getColumnOffset(idx)));
+        return;
+      case INT64:
+        sb.append(Bytes.getLong(rowAlloc, schema.getColumnOffset(idx)));
+        return;
+      case UNIXTIME_MICROS:
+        sb.append(RowResult.timestampToString(
+            Bytes.getLong(rowAlloc, schema.getColumnOffset(idx))));
+        return;
+      case FLOAT:
+        sb.append(Bytes.getFloat(rowAlloc, schema.getColumnOffset(idx)));
+        return;
+      case DOUBLE:
+        sb.append(Bytes.getDouble(rowAlloc, schema.getColumnOffset(idx)));
+        return;
+      case BINARY:
+      case STRING:
+        ByteBuffer value = getVarLengthData().get(idx).duplicate();
+        value.reset(); // Make sure we start at the beginning.
+        byte[] data = new byte[value.limit()];
+        value.get(data);
+        if (col.getType() == Type.STRING) {
+          sb.append('"');
+          StringUtil.appendEscapedSQLString(Bytes.getString(data), sb);
+          sb.append('"');
+        } else {
+          sb.append(Bytes.pretty(data));
+        }
+        return;
+      default:
+        throw new RuntimeException("unreachable");
     }
   }
 
@@ -693,7 +726,7 @@ public class PartialRow {
   boolean incrementColumn(int index) {
     Type type = schema.getColumnByIndex(index).getType();
     Preconditions.checkState(isSet(index));
-    int offset = getPositionInRowAllocAndSetBitSet(index);
+    int offset = schema.getColumnOffset(index);
     switch (type) {
       case BOOL: {
         boolean isFalse = rowAlloc[offset] == 0;
@@ -754,6 +787,7 @@ public class PartialRow {
       case STRING:
       case BINARY: {
         ByteBuffer data = varLengthData.get(index);
+        data.reset();
         int len = data.limit() - data.position();
         byte[] incremented = new byte[len + 1];
         System.arraycopy(data.array(), data.arrayOffset() + data.position(), 
incremented, 0, len);
@@ -766,6 +800,174 @@ public class PartialRow {
   }
 
   /**
+   * Returns {@code true} if the upper row is equal to the incremented lower
+   * row. Neither row is modified.
+   * @param lower the lower row
+   * @param upper the upper, possibly incremented, row
+   * @param indexes the columns in key order
+   * @return whether the upper row is equal to the incremented lower row
+   */
+  static boolean isIncremented(PartialRow lower, PartialRow upper, 
List<Integer> indexes) {
+    boolean equals = false;
+    ListIterator<Integer> iter = indexes.listIterator(indexes.size());
+    while (iter.hasPrevious()) {
+      int index = iter.previous();
+      if (equals) {
+        if (isCellEqual(lower, upper, index)) {
+          continue;
+        }
+        return false;
+      }
+
+      if (!lower.isSet(index) && !upper.isSet(index)) {
+        continue;
+      }
+      if (!isCellIncremented(lower, upper, index)) {
+        return false;
+      }
+      equals = true;
+    }
+    return equals;
+  }
+
+  /**
+   * Checks if the specified cell is equal in both rows.
+   * @param a a row
+   * @param b a row
+   * @param index the column index
+   * @return {@code true} if the cell values for the given column are equal
+   */
+  private static boolean isCellEqual(PartialRow a, PartialRow b, int index) {
+    // These checks are perhaps overly restrictive, but right now we only use
+    // this method for checking fully-set keys.
+    Preconditions.checkArgument(a.getSchema().equals(b.getSchema()));
+    Preconditions.checkArgument(a.getSchema().getColumnByIndex(index).isKey());
+    Preconditions.checkArgument(a.isSet(index));
+    Preconditions.checkArgument(b.isSet(index));
+
+    Type type = a.getSchema().getColumnByIndex(index).getType();
+    int offset = a.getSchema().getColumnOffset(index);
+
+    switch (type) {
+      case BOOL:
+        return a.rowAlloc[offset] == b.rowAlloc[offset];
+      case INT8:
+        return a.rowAlloc[offset] == b.rowAlloc[offset];
+      case INT16:
+        return Bytes.getShort(a.rowAlloc, offset) == 
Bytes.getShort(b.rowAlloc, offset);
+      case INT32:
+        return Bytes.getInt(a.rowAlloc, offset) == Bytes.getInt(b.rowAlloc, 
offset);
+      case INT64:
+      case UNIXTIME_MICROS:
+        return Bytes.getLong(a.rowAlloc, offset) == Bytes.getLong(b.rowAlloc, 
offset);
+      case FLOAT:
+        return Bytes.getFloat(a.rowAlloc, offset) == 
Bytes.getFloat(b.rowAlloc, offset);
+      case DOUBLE:
+        return Bytes.getDouble(a.rowAlloc, offset) == 
Bytes.getDouble(b.rowAlloc, offset);
+      case STRING:
+      case BINARY: {
+        ByteBuffer aData = a.varLengthData.get(index).duplicate();
+        ByteBuffer bData = b.varLengthData.get(index).duplicate();
+        aData.reset();
+        bData.reset();
+        int aLen = aData.limit() - aData.position();
+        int bLen = bData.limit() - bData.position();
+
+        if (aLen != bLen) {
+          return false;
+        }
+        for (int i = 0; i < aLen; i++) {
+          if (aData.get(aData.position() + i) != bData.get(bData.position() + 
i)) {
+            return false;
+          }
+        }
+        return true;
+      }
+      default:
+        throw new RuntimeException("unreachable");
+    }
+  }
+
+  /**
+   * Checks if the specified cell is in the upper row is an incremented version
+   * of the cell in the lower row.
+   * @param lower the lower row
+   * @param upper the possibly incremented upper row
+   * @param index the index of the column to check
+   * @return {@code true} if the column cell value in the upper row is equal to
+   *         the value in the lower row, incremented by one.
+   */
+  private static boolean isCellIncremented(PartialRow lower, PartialRow upper, 
int index) {
+    // These checks are perhaps overly restrictive, but right now we only use
+    // this method for checking fully-set keys.
+    Preconditions.checkArgument(lower.getSchema().equals(upper.getSchema()));
+    
Preconditions.checkArgument(lower.getSchema().getColumnByIndex(index).isKey());
+    Preconditions.checkArgument(lower.isSet(index));
+    Preconditions.checkArgument(upper.isSet(index));
+
+    Type type = lower.getSchema().getColumnByIndex(index).getType();
+    int offset = lower.getSchema().getColumnOffset(index);
+
+    switch (type) {
+      case BOOL: return lower.rowAlloc[offset] + 1 == upper.rowAlloc[offset];
+      case INT8: {
+        byte val = lower.rowAlloc[offset];
+        return val != Byte.MAX_VALUE && val + 1 == upper.rowAlloc[offset];
+      }
+      case INT16: {
+        short val = Bytes.getShort(lower.rowAlloc, offset);
+        return val != Short.MAX_VALUE && val + 1 == 
Bytes.getShort(upper.rowAlloc, offset);
+      }
+      case INT32: {
+        int val = Bytes.getInt(lower.rowAlloc, offset);
+        return val != Integer.MAX_VALUE && val + 1 == 
Bytes.getInt(upper.rowAlloc, offset);
+      }
+      case INT64:
+      case UNIXTIME_MICROS: {
+        long val = Bytes.getLong(lower.rowAlloc, offset);
+        return val != Long.MAX_VALUE && val + 1 == 
Bytes.getLong(upper.rowAlloc, offset);
+      }
+      case FLOAT: {
+        float val = Bytes.getFloat(lower.rowAlloc, offset);
+        return val != Float.POSITIVE_INFINITY &&
+               Math.nextAfter(val, Float.POSITIVE_INFINITY) ==
+                   Bytes.getFloat(upper.rowAlloc, offset);
+      }
+      case DOUBLE: {
+        double val = Bytes.getDouble(lower.rowAlloc, offset);
+        return val != Double.POSITIVE_INFINITY &&
+               Math.nextAfter(val, Double.POSITIVE_INFINITY) ==
+                   Bytes.getDouble(upper.rowAlloc, offset);
+      }
+      case STRING:
+      case BINARY: {
+        // Check that b is 1 byte bigger than a, the extra byte is 0, and the 
other bytes are equal.
+        ByteBuffer aData = lower.varLengthData.get(index).duplicate();
+        ByteBuffer bData = upper.varLengthData.get(index).duplicate();
+        aData.reset();
+        bData.reset();
+        int aLen = aData.limit() - aData.position();
+        int bLen = bData.limit() - bData.position();
+
+        if (aLen == Integer.MAX_VALUE ||
+            aLen + 1 != bLen ||
+            bData.get(bData.limit() - 1) != 0) {
+          return false;
+        }
+
+        for (int i = 0; i < aLen; i++) {
+          if (aData.get(aData.position() + i) != bData.get(bData.position() + 
i)) {
+            return false;
+          }
+        }
+        return true;
+      }
+      default:
+        throw new RuntimeException("unreachable");
+    }
+  }
+
+  /**
    * Get the schema used for this row.
    * @return a schema that came from KuduTable
    */

http://git-wip-us.apache.org/repos/asf/kudu/blob/6be3f328/java/kudu-client/src/main/java/org/apache/kudu/client/Partition.java
----------------------------------------------------------------------
diff --git 
a/java/kudu-client/src/main/java/org/apache/kudu/client/Partition.java 
b/java/kudu-client/src/main/java/org/apache/kudu/client/Partition.java
index 90f570a..1c4f810 100644
--- a/java/kudu-client/src/main/java/org/apache/kudu/client/Partition.java
+++ b/java/kudu-client/src/main/java/org/apache/kudu/client/Partition.java
@@ -17,11 +17,13 @@
 
 package org.apache.kudu.client;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 
 import com.google.common.base.Objects;
 
+import org.apache.kudu.Schema;
 import org.apache.kudu.annotations.InterfaceAudience;
 import org.apache.kudu.annotations.InterfaceStability;
 
@@ -185,4 +187,87 @@ public class Partition implements Comparable<Partition> {
                          partitionKeyStart.length == 0 ? "<start>" : 
Bytes.hex(partitionKeyStart),
                          partitionKeyEnd.length == 0 ? "<end>" : 
Bytes.hex(partitionKeyEnd));
   }
+
+  /**
+   * Formats the range partition into a string suitable for debug printing.
+   *
+   * @param table that this partition belongs to
+   * @return a string containing a formatted representation of the range 
partition
+   */
+  String formatRangePartition(KuduTable table) {
+    Schema schema = table.getSchema();
+    PartitionSchema partitionSchema = table.getPartitionSchema();
+    PartitionSchema.RangeSchema rangeSchema = partitionSchema.getRangeSchema();
+
+    if (rangeSchema.getColumns().isEmpty()) {
+      return "";
+    }
+    if (rangeKeyStart.length == 0 && rangeKeyEnd.length == 0) {
+      return "UNBOUNDED";
+    }
+
+    List<Integer> idxs = new ArrayList<>();
+    for (int id : partitionSchema.getRangeSchema().getColumns()) {
+      idxs.add(schema.getColumnIndex(id));
+    }
+
+    int numColumns = rangeSchema.getColumns().size();
+    StringBuilder sb = new StringBuilder();
+
+    if (rangeKeyEnd.length == 0) {
+      sb.append("VALUES >= ");
+      if (numColumns > 1) {
+        sb.append('(');
+      }
+      KeyEncoder.decodeRangePartitionKey(schema, partitionSchema, 
rangeKeyStart)
+                .appendShortDebugString(idxs, sb);
+      if (numColumns > 1) {
+        sb.append(')');
+      }
+    } else if (rangeKeyStart.length == 0) {
+      sb.append("VALUES < ");
+      if (numColumns > 1) {
+        sb.append('(');
+      }
+      KeyEncoder.decodeRangePartitionKey(schema, partitionSchema, rangeKeyEnd)
+                .appendShortDebugString(idxs, sb);
+      if (numColumns > 1) {
+        sb.append(')');
+      }
+    } else {
+      PartialRow lowerBound =
+          KeyEncoder.decodeRangePartitionKey(schema, partitionSchema, 
rangeKeyStart);
+      PartialRow upperBound =
+          KeyEncoder.decodeRangePartitionKey(schema, partitionSchema, 
rangeKeyEnd);
+
+      if (PartialRow.isIncremented(lowerBound, upperBound, idxs)) {
+        sb.append("VALUES = ");
+        if (numColumns > 1) {
+          sb.append('(');
+        }
+        lowerBound.appendShortDebugString(idxs, sb);
+        if (numColumns > 1) {
+          sb.append(')');
+        }
+      } else {
+        if (numColumns > 1) {
+          sb.append('(');
+        }
+        lowerBound.appendShortDebugString(idxs, sb);
+        if (numColumns > 1) {
+          sb.append(')');
+        }
+        sb.append(" <= VALUES < ");
+        if (numColumns > 1) {
+          sb.append('(');
+        }
+        upperBound.appendShortDebugString(idxs, sb);
+        if (numColumns > 1) {
+          sb.append(')');
+        }
+      }
+    }
+
+    return sb.toString();
+  }
 }

http://git-wip-us.apache.org/repos/asf/kudu/blob/6be3f328/java/kudu-client/src/main/java/org/apache/kudu/client/RowResult.java
----------------------------------------------------------------------
diff --git 
a/java/kudu-client/src/main/java/org/apache/kudu/client/RowResult.java 
b/java/kudu-client/src/main/java/org/apache/kudu/client/RowResult.java
index 898a3fc..d85e777 100644
--- a/java/kudu-client/src/main/java/org/apache/kudu/client/RowResult.java
+++ b/java/kudu-client/src/main/java/org/apache/kudu/client/RowResult.java
@@ -513,7 +513,7 @@ public class RowResult {
 
   /**
    * Transforms a timestamp into a string, whose formatting and timezone is 
consistent
-   * across kudu.
+   * across Kudu.
    * @param timestamp the timestamp, in microseconds
    * @return a string, in the format: YYYY-MM-DDTHH:MM:SS.ssssssZ
    */

http://git-wip-us.apache.org/repos/asf/kudu/blob/6be3f328/java/kudu-client/src/main/java/org/apache/kudu/util/StringUtil.java
----------------------------------------------------------------------
diff --git 
a/java/kudu-client/src/main/java/org/apache/kudu/util/StringUtil.java 
b/java/kudu-client/src/main/java/org/apache/kudu/util/StringUtil.java
new file mode 100644
index 0000000..67027af
--- /dev/null
+++ b/java/kudu-client/src/main/java/org/apache/kudu/util/StringUtil.java
@@ -0,0 +1,90 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.kudu.util;
+
+import org.apache.kudu.annotations.InterfaceAudience;
+
[email protected]
+public class StringUtil {
+
+  /**
+   * Escapes the provided string and appends it to the string builder. The
+   * escaping is done according to the Hive/Impala escaping rules. Adapted from
+   * org.apache.hadoop.hive.ql.parse.BaseSemanticAnalyzer.escapeSQLString, with
+   * one difference: '%' and '_' are not escaped, since the resulting escaped
+   * string should not be used for a LIKE statement.
+   */
+  public static void appendEscapedSQLString(String s, StringBuilder sb) {
+    for (int i = 0; i < s.length(); i++) {
+      char currentChar = s.charAt(i);
+      switch (currentChar) {
+        case '\0': {
+          sb.append("\\0");
+          break;
+        }
+        case '\'': {
+          sb.append("\\'");
+          break;
+        }
+        case '\"': {
+          sb.append("\\\"");
+          break;
+        }
+        case '\b': {
+          sb.append("\\b");
+          break;
+        }
+        case '\n': {
+          sb.append("\\n");
+          break;
+        }
+        case '\r': {
+          sb.append("\\r");
+          break;
+        }
+        case '\t': {
+          sb.append("\\t");
+          break;
+        }
+        case '\\': {
+          sb.append("\\\\");
+          break;
+        }
+        case '\u001A': {
+          sb.append("\\Z");
+          break;
+        }
+        default: {
+          if (currentChar < ' ') {
+            sb.append("\\u");
+            String hex = Integer.toHexString(currentChar);
+            for (int j = 4; j > hex.length(); --j) {
+              sb.append('0');
+            }
+            sb.append(hex);
+          } else {
+            sb.append(currentChar);
+          }
+        }
+      }
+    }
+  }
+
+  /** Non-constructable utility class. */
+  private StringUtil() { }
+}

http://git-wip-us.apache.org/repos/asf/kudu/blob/6be3f328/java/kudu-client/src/test/java/org/apache/kudu/client/TestKuduTable.java
----------------------------------------------------------------------
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 96d62de..3634e95 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
@@ -16,16 +16,13 @@
 // under the License.
 package org.apache.kudu.client;
 
-import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
+import static org.junit.Assert.*;
 
 import java.util.ArrayList;
 import java.util.List;
 
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
 import org.junit.BeforeClass;
 import org.junit.Rule;
 import org.junit.Test;
@@ -323,6 +320,168 @@ public class TestKuduTable extends BaseKuduTest {
     assertTrue(response.getRowError().getErrorStatus().isNotFound());
   }
 
+  @Test(timeout = 100000)
+  public void testFormatRangePartitions() throws Exception {
+    String tableName = name.getMethodName() + System.currentTimeMillis();
+    CreateTableOptions builder = getBasicCreateTableOptions();
+    List<String> expected = Lists.newArrayList();
+
+    {
+      expected.add("VALUES < -300");
+      PartialRow upper = basicSchema.newPartialRow();
+      upper.addInt(0, -300);
+      builder.addRangePartition(basicSchema.newPartialRow(), upper);
+    }
+    {
+      expected.add("-100 <= VALUES < 0");
+      PartialRow lower = basicSchema.newPartialRow();
+      lower.addInt(0, -100);
+      PartialRow upper = basicSchema.newPartialRow();
+      upper.addInt(0, 0);
+      builder.addRangePartition(lower, upper);
+    }
+    {
+      expected.add("0 <= VALUES < 100");
+      PartialRow lower = basicSchema.newPartialRow();
+      lower.addInt(0, -1);
+      PartialRow upper = basicSchema.newPartialRow();
+      upper.addInt(0, 99);
+      builder.addRangePartition(lower, upper,
+                                RangePartitionBound.EXCLUSIVE_BOUND,
+                                RangePartitionBound.INCLUSIVE_BOUND);
+    }
+    {
+      expected.add("VALUES = 300");
+      PartialRow lower = basicSchema.newPartialRow();
+      lower.addInt(0, 300);
+      PartialRow upper = basicSchema.newPartialRow();
+      upper.addInt(0, 300);
+      builder.addRangePartition(lower, upper,
+                                RangePartitionBound.INCLUSIVE_BOUND,
+                                RangePartitionBound.INCLUSIVE_BOUND);
+    }
+    {
+      expected.add("VALUES >= 400");
+      PartialRow lower = basicSchema.newPartialRow();
+      lower.addInt(0, 400);
+      builder.addRangePartition(lower, basicSchema.newPartialRow());
+    }
+
+    syncClient.createTable(tableName, basicSchema, builder);
+    assertEquals(
+        expected,
+        syncClient.openTable(tableName).getFormattedRangePartitions(10000));
+  }
+
+  @Test(timeout = 100000)
+  public void testFormatRangePartitionsCompoundColumns() throws Exception {
+    String tableName = name.getMethodName() + System.currentTimeMillis();
+
+    ArrayList<ColumnSchema> columns = new ArrayList<>();
+    columns.add(new ColumnSchema.ColumnSchemaBuilder("a", 
Type.STRING).key(true).build());
+    columns.add(new ColumnSchema.ColumnSchemaBuilder("b", 
Type.INT8).key(true).build());
+    Schema schema = new Schema(columns);
+
+    CreateTableOptions builder = new CreateTableOptions();
+    builder.addHashPartitions(ImmutableList.of("a"), 2);
+    builder.addHashPartitions(ImmutableList.of("b"), 2);
+    builder.setRangePartitionColumns(ImmutableList.of("a", "b"));
+    List<String> expected = Lists.newArrayList();
+
+    {
+      expected.add("VALUES < (\"\", -100)");
+      PartialRow upper = schema.newPartialRow();
+      upper.addString(0, "");
+      upper.addByte(1, (byte) -100);
+      builder.addRangePartition(schema.newPartialRow(), upper);
+    }
+    {
+      expected.add("VALUES = (\"abc\", 0)");
+      PartialRow lower = schema.newPartialRow();
+      lower.addString(0, "abc");
+      lower.addByte(1, (byte) 0);
+      PartialRow upper = schema.newPartialRow();
+      upper.addString(0, "abc");
+      upper.addByte(1, (byte) 1);
+      builder.addRangePartition(lower, upper);
+    }
+    {
+      expected.add("(\"def\", 0) <= VALUES < (\"ghi\", 100)");
+      PartialRow lower = schema.newPartialRow();
+      lower.addString(0, "def");
+      lower.addByte(1, (byte) -1);
+      PartialRow upper = schema.newPartialRow();
+      upper.addString(0, "ghi");
+      upper.addByte(1, (byte) 99);
+      builder.addRangePartition(lower, upper,
+                                RangePartitionBound.EXCLUSIVE_BOUND,
+                                RangePartitionBound.INCLUSIVE_BOUND);
+    }
+
+    syncClient.createTable(tableName, schema, builder);
+    assertEquals(
+        expected,
+        syncClient.openTable(tableName).getFormattedRangePartitions(10000));
+  }
+
+  @Test(timeout = 100000)
+  public void testFormatRangePartitionsStringColumn() throws Exception {
+    String tableName = name.getMethodName() + System.currentTimeMillis();
+
+    ArrayList<ColumnSchema> columns = new ArrayList<>();
+    columns.add(new ColumnSchema.ColumnSchemaBuilder("a", 
Type.STRING).key(true).build());
+    Schema schema = new Schema(columns);
+
+    CreateTableOptions builder = new CreateTableOptions();
+    builder.setRangePartitionColumns(ImmutableList.of("a"));
+    List<String> expected = Lists.newArrayList();
+
+    {
+      expected.add("VALUES < \"\\0\"");
+      PartialRow upper = schema.newPartialRow();
+      upper.addString(0, "\0");
+      builder.addRangePartition(schema.newPartialRow(), upper);
+    }
+    {
+      expected.add("VALUES = \"abc\"");
+      PartialRow lower = schema.newPartialRow();
+      lower.addString(0, "abc");
+      PartialRow upper = schema.newPartialRow();
+      upper.addString(0, "abc\0");
+      builder.addRangePartition(lower, upper);
+    }
+    {
+      expected.add("\"def\" <= VALUES < \"ghi\"");
+      PartialRow lower = schema.newPartialRow();
+      lower.addString(0, "def");
+      PartialRow upper = schema.newPartialRow();
+      upper.addString(0, "ghi");
+      builder.addRangePartition(lower, upper);
+    }
+    {
+      expected.add("VALUES >= \"z\"");
+      PartialRow lower = schema.newPartialRow();
+      lower.addString(0, "z");
+      builder.addRangePartition(lower, schema.newPartialRow());
+    }
+
+    syncClient.createTable(tableName, schema, builder);
+    assertEquals(
+        expected,
+        syncClient.openTable(tableName).getFormattedRangePartitions(10000));
+  }
+
+  @Test(timeout = 100000)
+  public void testFormatRangePartitionsUnbounded() throws Exception {
+    String tableName = name.getMethodName() + System.currentTimeMillis();
+    CreateTableOptions builder = getBasicCreateTableOptions();
+    syncClient.createTable(tableName, basicSchema, builder);
+
+    assertEquals(
+        ImmutableList.of("UNBOUNDED"),
+        syncClient.openTable(tableName).getFormattedRangePartitions(10000));
+  }
+
   public KuduTable createTableWithSplitsAndTest(int splitsCount) throws 
Exception {
     String tableName = name.getMethodName() + System.currentTimeMillis();
     CreateTableOptions builder = getBasicCreateTableOptions();

http://git-wip-us.apache.org/repos/asf/kudu/blob/6be3f328/java/kudu-client/src/test/java/org/apache/kudu/client/TestOperation.java
----------------------------------------------------------------------
diff --git 
a/java/kudu-client/src/test/java/org/apache/kudu/client/TestOperation.java 
b/java/kudu-client/src/test/java/org/apache/kudu/client/TestOperation.java
index 44b204a..1ac314c 100644
--- a/java/kudu-client/src/test/java/org/apache/kudu/client/TestOperation.java
+++ b/java/kudu-client/src/test/java/org/apache/kudu/client/TestOperation.java
@@ -149,7 +149,7 @@ public class TestOperation {
     row.addBinary("c6", Bytes.fromString("c6_val"));
 
     assertEquals("(int8 c0=1, int16 c1=2, int32 c2=3, int64 c3=4, " +
-                 "unixtime_micros c4=1970-01-01T00:00:00.000005Z, string 
c5=c5_val, " +
+                 "unixtime_micros c4=1970-01-01T00:00:00.000005Z, string 
c5=\"c5_val\", " +
                  "binary c6=\"c6_val\")",
                  insert.getRow().stringifyRowKey());
 

http://git-wip-us.apache.org/repos/asf/kudu/blob/6be3f328/java/kudu-client/src/test/java/org/apache/kudu/util/TestStringUtil.java
----------------------------------------------------------------------
diff --git 
a/java/kudu-client/src/test/java/org/apache/kudu/util/TestStringUtil.java 
b/java/kudu-client/src/test/java/org/apache/kudu/util/TestStringUtil.java
new file mode 100644
index 0000000..138fc11
--- /dev/null
+++ b/java/kudu-client/src/test/java/org/apache/kudu/util/TestStringUtil.java
@@ -0,0 +1,40 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+package org.apache.kudu.util;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+public class TestStringUtil {
+
+  private String escapeSQLString(String s) {
+    StringBuilder sb = new StringBuilder();
+    StringUtil.appendEscapedSQLString(s, sb);
+    return sb.toString();
+  }
+
+  @Test
+  public void testAppendEscapedSQLString() {
+    assertEquals("", escapeSQLString(""));
+    assertEquals("a", escapeSQLString("a"));
+    assertEquals("\\n", escapeSQLString("\n"));
+    assertEquals("the_quick brown\\tfox\\njumps\\rover\\bthe\\0lazy\\\\dog",
+                 escapeSQLString("the_quick 
brown\tfox\njumps\rover\bthe\0lazy\\dog"));
+    assertEquals("\\u0012\\0", escapeSQLString("\u0012\u0000"));
+  }
+}

Reply via email to