kudu-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From danburk...@apache.org
Subject [3/3] incubator-kudu git commit: [java-client] implement KuduPredicate API
Date Fri, 01 Apr 2016 20:07:32 GMT
[java-client] implement KuduPredicate API

This commit adds a new class KuduPredicate, which corresponds to the new
ColumnPredicatePB predicate type for scans. The KuduPredicate allows specifying
exclusive upper bound and lower bound predicates on columns. This commit is a
first step towards predicate optimization and partition pruning, but stops short
of actually implementing those optimizations.

This commit does not attempt to change any callers of the now deprecated
ColumnRangePredicate API.

Change-Id: Icdca28139a2f4f15633cfd872e372429bad831cd
Reviewed-on: http://gerrit.cloudera.org:8080/2591
Tested-by: Kudu Jenkins
Reviewed-by: Jean-Daniel Cryans


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

Branch: refs/heads/master
Commit: 16c03cda041f901247d83bf37c1958697a3a96dc
Parents: dc58069
Author: Dan Burkert <dan@cloudera.com>
Authored: Fri Mar 4 16:00:55 2016 -0800
Committer: Dan Burkert <dan@cloudera.com>
Committed: Fri Apr 1 19:56:59 2016 +0000

----------------------------------------------------------------------
 .../client/AbstractKuduScannerBuilder.java      |  35 +-
 .../org/kududb/client/AsyncKuduScanner.java     |  63 +-
 .../src/main/java/org/kududb/client/Bytes.java  |  40 +-
 .../org/kududb/client/ColumnRangePredicate.java |  68 +-
 .../java/org/kududb/client/KuduPredicate.java   | 678 +++++++++++++++++++
 .../java/org/kududb/client/KuduScanner.java     |   2 +-
 .../java/org/kududb/client/ProtobufHelper.java  |  31 +-
 .../java/org/kududb/client/BaseKuduTest.java    |   9 +-
 .../test/java/org/kududb/client/TestBytes.java  |   8 +
 .../java/org/kududb/client/TestKuduClient.java  |  51 ++
 .../org/kududb/client/TestKuduPredicate.java    | 628 +++++++++++++++++
 .../org/kududb/client/TestScanPredicate.java    | 602 ++++++++++++++++
 .../src/test/resources/log4j.properties         |   1 -
 13 files changed, 2154 insertions(+), 62 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-kudu/blob/16c03cda/java/kudu-client/src/main/java/org/kududb/client/AbstractKuduScannerBuilder.java
----------------------------------------------------------------------
diff --git a/java/kudu-client/src/main/java/org/kududb/client/AbstractKuduScannerBuilder.java b/java/kudu-client/src/main/java/org/kududb/client/AbstractKuduScannerBuilder.java
index c4c17ef..ae65aaf 100644
--- a/java/kudu-client/src/main/java/org/kududb/client/AbstractKuduScannerBuilder.java
+++ b/java/kudu-client/src/main/java/org/kududb/client/AbstractKuduScannerBuilder.java
@@ -16,8 +16,9 @@
 // under the License.
 package org.kududb.client;
 
-import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 import com.google.common.collect.ImmutableList;
 import org.kududb.annotations.InterfaceAudience;
@@ -34,7 +35,9 @@ public abstract class AbstractKuduScannerBuilder
     <S extends AbstractKuduScannerBuilder<? super S, T>, T> {
   final AsyncKuduClient client;
   final KuduTable table;
-  final List<Tserver.ColumnRangePredicatePB> columnRangePredicates;
+
+  /** Map of column name to predicate */
+  final Map<String, KuduPredicate> predicates = new HashMap<>();
 
   AsyncKuduScanner.ReadMode readMode = AsyncKuduScanner.ReadMode.READ_LATEST;
   int batchSizeBytes = 1024*1024;
@@ -53,7 +56,6 @@ public abstract class AbstractKuduScannerBuilder
   AbstractKuduScannerBuilder(AsyncKuduClient client, KuduTable table) {
     this.client = client;
     this.table = table;
-    this.columnRangePredicates = new ArrayList<>();
     this.scanRequestTimeout = client.getDefaultOperationTimeoutMs();
   }
 
@@ -71,10 +73,11 @@ public abstract class AbstractKuduScannerBuilder
    * Adds a predicate for a column.
    * @param predicate predicate for a column to add
    * @return this instance
+   * @deprecated use {@link #addPredicate(KuduPredicate)}
    */
+  @Deprecated
   public S addColumnRangePredicate(ColumnRangePredicate predicate) {
-    columnRangePredicates.add(predicate.getPb());
-    return (S) this;
+    return addPredicate(predicate.toKuduPredicate());
   }
 
   /**
@@ -85,9 +88,25 @@ public abstract class AbstractKuduScannerBuilder
    * @throws IllegalArgumentException thrown when the passed bytes aren't valid
    */
   public S addColumnRangePredicatesRaw(byte[] predicateBytes) {
-    List<Tserver.ColumnRangePredicatePB> predicates =
-        ColumnRangePredicate.fromByteArray(predicateBytes);
-    columnRangePredicates.addAll(predicates);
+    for (Tserver.ColumnRangePredicatePB pb : ColumnRangePredicate.fromByteArray(predicateBytes)) {
+      addPredicate(ColumnRangePredicate.fromPb(pb).toKuduPredicate());
+    }
+    return (S) this;
+  }
+
+  /**
+   * Adds a predicate to the scan.
+   * @param predicate predicate to add
+   * @return this instance
+   */
+  public S addPredicate(KuduPredicate predicate) {
+    String columnName = predicate.getColumn().getName();
+    KuduPredicate existing = predicates.get(columnName);
+    if (existing == null) {
+      predicates.put(columnName, predicate);
+    } else {
+      predicates.put(columnName, existing.merge(predicate));
+    }
     return (S) this;
   }
 

http://git-wip-us.apache.org/repos/asf/incubator-kudu/blob/16c03cda/java/kudu-client/src/main/java/org/kududb/client/AsyncKuduScanner.java
----------------------------------------------------------------------
diff --git a/java/kudu-client/src/main/java/org/kududb/client/AsyncKuduScanner.java b/java/kudu-client/src/main/java/org/kududb/client/AsyncKuduScanner.java
index 33c4f73..cabca8d 100644
--- a/java/kudu-client/src/main/java/org/kududb/client/AsyncKuduScanner.java
+++ b/java/kudu-client/src/main/java/org/kududb/client/AsyncKuduScanner.java
@@ -25,27 +25,33 @@
  */
 package org.kududb.client;
 
-import java.util.ArrayList;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.List;
-
+import com.google.common.collect.ImmutableList;
 import com.google.protobuf.Message;
 import com.google.protobuf.ZeroCopyLiteralByteString;
+import com.stumbleupon.async.Callback;
+import com.stumbleupon.async.Deferred;
+import org.jboss.netty.buffer.ChannelBuffer;
 import org.kududb.ColumnSchema;
 import org.kududb.Common;
 import org.kududb.Schema;
-import com.stumbleupon.async.Callback;
-import com.stumbleupon.async.Deferred;
 import org.kududb.annotations.InterfaceAudience;
 import org.kududb.annotations.InterfaceStability;
 import org.kududb.tserver.Tserver;
 import org.kududb.util.Pair;
-import org.jboss.netty.buffer.ChannelBuffer;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+
 import static com.google.common.base.Preconditions.checkArgument;
-import static org.kududb.tserver.Tserver.*;
+import static org.kududb.tserver.Tserver.NewScanRequestPB;
+import static org.kududb.tserver.Tserver.ScanRequestPB;
+import static org.kududb.tserver.Tserver.ScanResponsePB;
+import static org.kududb.tserver.Tserver.TabletServerErrorPB;
 
 /**
  * Creates a scanner to read data from Kudu.
@@ -133,7 +139,11 @@ public final class AsyncKuduScanner {
   private final AsyncKuduClient client;
   private final KuduTable table;
   private final Schema schema;
-  private final List<Tserver.ColumnRangePredicatePB> columnRangePredicates;
+
+  /**
+   * Map of column name to predicate.
+   */
+  private final Map<String, KuduPredicate> predicates;
 
   /**
    * Maximum number of bytes returned by the scanner, on each batch.
@@ -220,7 +230,7 @@ public final class AsyncKuduScanner {
 
   AsyncKuduScanner(AsyncKuduClient client, KuduTable table, List<String> projectedNames,
                    List<Integer> projectedIndexes, ReadMode readMode, long scanRequestTimeout,
-                   List<Tserver.ColumnRangePredicatePB> columnRangePredicates, long limit,
+                   Map<String, KuduPredicate> predicates, long limit,
                    boolean cacheBlocks, boolean prefetching,
                    byte[] startPrimaryKey, byte[] endPrimaryKey,
                    byte[] startPartitionKey, byte[] endPartitionKey,
@@ -240,7 +250,7 @@ public final class AsyncKuduScanner {
     this.table = table;
     this.readMode = readMode;
     this.scanRequestTimeout = scanRequestTimeout;
-    this.columnRangePredicates = columnRangePredicates;
+    this.predicates = predicates;
     this.limit = limit;
     this.cacheBlocks = cacheBlocks;
     this.prefetching = prefetching;
@@ -314,6 +324,22 @@ public final class AsyncKuduScanner {
     } else {
       this.schema = table.getSchema();
     }
+
+    // If any of the column predicates are of type None (the predicate is known
+    // to match no rows), then the scan can be short circuited without
+    // contacting any tablet servers.
+    boolean shortCircuit = false;
+    for (KuduPredicate predicate : this.predicates.values()) {
+      if (predicate.getType() == KuduPredicate.PredicateType.NONE) {
+        shortCircuit = true;
+        break;
+      }
+    }
+    if (shortCircuit) {
+      LOG.debug("Short circuiting scan with predicates: {}", predicates.values());
+      this.hasMore = false;
+      this.closed = true;
+    }
   }
 
   /**
@@ -665,6 +691,15 @@ public final class AsyncKuduScanner {
       return "Scan";
     }
 
+    @Override
+    Collection<Integer> getRequiredFeatures() {
+      if (predicates.isEmpty()) {
+        return ImmutableList.of();
+      } else {
+        return ImmutableList.of(Tserver.TabletServerFeatures.COLUMN_PREDICATES_VALUE);
+      }
+    }
+
     /** Serializes this request.  */
     ChannelBuffer serialize(Message header) {
       final ScanRequestPB.Builder builder = ScanRequestPB.newBuilder();
@@ -701,8 +736,8 @@ public final class AsyncKuduScanner {
             newBuilder.setStopPrimaryKey(ZeroCopyLiteralByteString.copyFrom(endPrimaryKey));
           }
 
-          if (!columnRangePredicates.isEmpty()) {
-            newBuilder.addAllDEPRECATEDRangePredicates(columnRangePredicates);
+          for (KuduPredicate pred : predicates.values()) {
+            newBuilder.addColumnPredicates(pred.toPB());
           }
           builder.setNewScanRequest(newBuilder.build())
                  .setBatchSizeBytes(batchSizeBytes);
@@ -791,7 +826,7 @@ public final class AsyncKuduScanner {
     public AsyncKuduScanner build() {
       return new AsyncKuduScanner(
           client, table, projectedColumnNames, projectedColumnIndexes, readMode,
-          scanRequestTimeout, columnRangePredicates, limit, cacheBlocks,
+          scanRequestTimeout, predicates, limit, cacheBlocks,
           prefetching, lowerBoundPrimaryKey, upperBoundPrimaryKey,
           lowerBoundPartitionKey, upperBoundPartitionKey,
           htTimestamp, batchSizeBytes);

http://git-wip-us.apache.org/repos/asf/incubator-kudu/blob/16c03cda/java/kudu-client/src/main/java/org/kududb/client/Bytes.java
----------------------------------------------------------------------
diff --git a/java/kudu-client/src/main/java/org/kududb/client/Bytes.java b/java/kudu-client/src/main/java/org/kududb/client/Bytes.java
index e327b9b..624536e 100644
--- a/java/kudu-client/src/main/java/org/kududb/client/Bytes.java
+++ b/java/kudu-client/src/main/java/org/kududb/client/Bytes.java
@@ -710,7 +710,7 @@ public final class Bytes {
   // Pretty-printing byte arrays. //
   // ---------------------------- //
 
-  private static final byte[] HEX = {
+  private static final char[] HEX = {
       '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
       'A', 'B', 'C', 'D', 'E', 'F'
   };
@@ -741,8 +741,8 @@ public final class Bytes {
         outbuf.append('\\').append('t');
       } else {
         outbuf.append("\\x")
-            .append((char) HEX[(b >>> 4) & 0x0F])
-            .append((char) HEX[b & 0x0F]);
+            .append(HEX[(b >>> 4) & 0x0F])
+            .append(HEX[b & 0x0F]);
       }
     }
     if (ascii < n / 2) {
@@ -793,25 +793,27 @@ public final class Bytes {
     return buf.toString();
   }
 
-  // This doesn't really belong here but it doesn't belong anywhere else
-  // either, so let's put it close to the other pretty-printing functions.
   /**
-   * Pretty-prints a {@code long} into a fixed-width hexadecimal number.
-   * @return A string of the form {@code 0x0123456789ABCDEF}.
+   * Convert a byte array to a hex encoded string.
+   *
+   * TODO: replace this with {@link com.google.common.io.BaseEncoding}
+   * when the Guava version is bumped.
+   *
+   * https://stackoverflow.com/questions/9655181/how-to-convert-a-byte-array-to-a-hex-string-in-java
+   * @param bytes the bytes to encode
+   * @return the hex encoded bytes
    */
-  public static String hex(long v) {
-    final byte[] buf = new byte[2 + 16];
-    buf[0] = '0';
-    buf[1] = 'x';
-    int i = 2 + 16;
-    do {
-      buf[--i] = HEX[(int) v & 0x0F];
-      v >>>= 4;
-    } while (v != 0);
-    for (/**/; i > 1; i--) {
-      buf[i] = '0';
+  public static String hex(byte[] bytes) {
+    StringBuilder sb = new StringBuilder(2 + bytes.length * 2);
+    sb.append('0');
+    sb.append('x');
+
+    for (byte b : bytes) {
+      int v = b & 0xFF;
+      sb.append(HEX[v >>> 4]);
+      sb.append(HEX[v & 0x0F]);
     }
-    return new String(buf);
+    return sb.toString();
   }
 
   // Ugly stuff

http://git-wip-us.apache.org/repos/asf/incubator-kudu/blob/16c03cda/java/kudu-client/src/main/java/org/kududb/client/ColumnRangePredicate.java
----------------------------------------------------------------------
diff --git a/java/kudu-client/src/main/java/org/kududb/client/ColumnRangePredicate.java b/java/kudu-client/src/main/java/org/kududb/client/ColumnRangePredicate.java
index 1d0daa3..0e24088 100644
--- a/java/kudu-client/src/main/java/org/kududb/client/ColumnRangePredicate.java
+++ b/java/kudu-client/src/main/java/org/kududb/client/ColumnRangePredicate.java
@@ -21,10 +21,8 @@ import com.google.protobuf.ZeroCopyLiteralByteString;
 import org.kududb.ColumnSchema;
 import org.kududb.Type;
 import org.kududb.annotations.InterfaceAudience;
-import org.kududb.annotations.InterfaceStability;
 import org.kududb.tserver.Tserver;
 
-import java.nio.ByteBuffer;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -32,9 +30,10 @@ import java.util.List;
 /**
  * A range predicate on one of the columns in the underlying data.
  * Both boundaries are inclusive.
+ * @deprecated use the {@link KuduPredicate} class instead.
  */
 @InterfaceAudience.Public
-@InterfaceStability.Evolving
+@Deprecated
 public class ColumnRangePredicate {
 
   private final Tserver.ColumnRangePredicatePB.Builder pb = Tserver.ColumnRangePredicatePB
@@ -63,6 +62,52 @@ public class ColumnRangePredicate {
   }
 
   /**
+   * Convert a bound into a {@link KuduPredicate}.
+   * @param column the column
+   * @param op the bound comparison operator
+   * @param bound the bound
+   * @return the {@code KuduPredicate}
+   */
+  private static KuduPredicate toKuduPredicate(ColumnSchema column,
+                                               KuduPredicate.ComparisonOp op,
+                                               byte[] bound) {
+    if (bound == null) { return null; }
+    switch (column.getType().getDataType()) {
+      case BOOL: return KuduPredicate.newComparisonPredicate(column, op, Bytes.getBoolean(bound));
+      case INT8: return KuduPredicate.newComparisonPredicate(column, op, Bytes.getByte(bound));
+      case INT16: return KuduPredicate.newComparisonPredicate(column, op, Bytes.getShort(bound));
+      case INT32: return KuduPredicate.newComparisonPredicate(column, op, Bytes.getInt(bound));
+      case INT64:
+      case TIMESTAMP: return KuduPredicate.newComparisonPredicate(column, op, Bytes.getLong(bound));
+      case FLOAT: return KuduPredicate.newComparisonPredicate(column, op, Bytes.getFloat(bound));
+      case DOUBLE: return KuduPredicate.newComparisonPredicate(column, op, Bytes.getDouble(bound));
+      case STRING: return KuduPredicate.newComparisonPredicate(column, op, Bytes.getString(bound));
+      case BINARY: return KuduPredicate.newComparisonPredicate(column, op, bound);
+      default:
+        throw new IllegalStateException(String.format("unknown column type %s", column.getType()));
+    }
+  }
+
+  /**
+   * Convert this column range predicate into a {@link KuduPredicate}.
+   * @return the column predicate.
+   */
+  public KuduPredicate toKuduPredicate() {
+    KuduPredicate lower =
+        toKuduPredicate(column, KuduPredicate.ComparisonOp.GREATER_EQUAL, lowerBound);
+    KuduPredicate upper =
+        toKuduPredicate(column, KuduPredicate.ComparisonOp.LESS_EQUAL, upperBound);
+
+    if (upper != null && lower != null) {
+      return upper.merge(lower);
+    } else if (upper != null) {
+      return upper;
+    } else {
+      return lower;
+    }
+  }
+
+  /**
    * Set a boolean for the lower bound
    * @param lowerBound value for the lower bound
    */
@@ -301,6 +346,23 @@ public class ColumnRangePredicate {
   }
 
   /**
+   * Creates a {@code ColumnRangePredicate} from a protobuf column range predicate message.
+   * @param pb the protobuf message
+   * @return a column range predicate
+   */
+  static ColumnRangePredicate fromPb(Tserver.ColumnRangePredicatePB pb) {
+    ColumnRangePredicate pred =
+        new ColumnRangePredicate(ProtobufHelper.pbToColumnSchema(pb.getColumn()));
+    if (pb.hasLowerBound()) {
+      pred.setLowerBoundInternal(pb.getLowerBound().toByteArray());
+    }
+    if (pb.hasInclusiveUpperBound()) {
+      pred.setUpperBoundInternal(pb.getInclusiveUpperBound().toByteArray());
+    }
+    return pred;
+  }
+
+  /**
    * Convert a list of predicates given in bytes back to its pb format. It also hides the
    * InvalidProtocolBufferException.
    */

http://git-wip-us.apache.org/repos/asf/incubator-kudu/blob/16c03cda/java/kudu-client/src/main/java/org/kududb/client/KuduPredicate.java
----------------------------------------------------------------------
diff --git a/java/kudu-client/src/main/java/org/kududb/client/KuduPredicate.java b/java/kudu-client/src/main/java/org/kududb/client/KuduPredicate.java
new file mode 100644
index 0000000..aa0801b
--- /dev/null
+++ b/java/kudu-client/src/main/java/org/kududb/client/KuduPredicate.java
@@ -0,0 +1,678 @@
+// 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.kududb.client;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
+import com.google.common.primitives.UnsignedBytes;
+import com.google.protobuf.ByteString;
+import org.kududb.ColumnSchema;
+import org.kududb.Common;
+import org.kududb.Schema;
+import org.kududb.Type;
+import org.kududb.annotations.InterfaceAudience;
+import org.kududb.annotations.InterfaceStability;
+
+import java.util.Arrays;
+
+/**
+ * A predicate which can be used to filter rows based on the value of a column.
+ */
+@InterfaceAudience.Public
+@InterfaceStability.Evolving
+public class KuduPredicate {
+
+  /**
+   * The predicate type.
+   */
+  enum PredicateType {
+    /** A predicate which filters all rows. */
+    NONE,
+    /** A predicate which filters all rows not equal to a value. */
+    EQUALITY,
+    /** A predicate which filters all rows not in a range. */
+    RANGE,
+    /** A predicate which filters all null rows. */
+    IS_NOT_NULL,
+  }
+
+  /**
+   * The comparison operator of a predicate.
+   */
+  public enum ComparisonOp {
+    GREATER,
+    GREATER_EQUAL,
+    EQUAL,
+    LESS,
+    LESS_EQUAL,
+  }
+
+  private final PredicateType type;
+  private final ColumnSchema column;
+
+  /**
+   * The inclusive lower bound value if this is a Range predicate, or
+   * the createEquality value if this is an Equality predicate.
+   */
+  private final byte[] lower;
+
+  /** The exclusive upper bound value if this is a Range predicate. */
+  private final byte[] upper;
+
+  /**
+   * Creates a new {@code KuduPredicate} on a boolean column.
+   * @param column the column schema
+   * @param op the comparison operation
+   * @param value the value to compare against
+   */
+  public static KuduPredicate newComparisonPredicate(ColumnSchema column,
+                                                     ComparisonOp op,
+                                                     boolean value) {
+    checkColumn(column, Type.BOOL);
+    // Create the comparison predicate. Range predicates on boolean values can
+    // always be converted to either an equality, an IS NOT NULL (filtering only
+    // null values), or NONE (filtering all values).
+    switch (op) {
+      case GREATER: {
+        // b > true  -> b NONE
+        // b > false -> b = true
+        if (value) {
+          return none(column);
+        } else {
+          return new KuduPredicate(PredicateType.EQUALITY, column, Bytes.fromBoolean(true), null);
+        }
+      }
+      case GREATER_EQUAL: {
+        // b >= true  -> b = true
+        // b >= false -> b IS NOT NULL
+        if (value) {
+          return new KuduPredicate(PredicateType.EQUALITY, column, Bytes.fromBoolean(true), null);
+        } else {
+          return newIsNotNullPredicate(column);
+        }
+      }
+      case EQUAL: return new KuduPredicate(PredicateType.EQUALITY, column,
+                                           Bytes.fromBoolean(value), null);
+      case LESS: {
+        // b < true  -> b NONE
+        // b < false -> b = true
+        if (value) {
+          return new KuduPredicate(PredicateType.EQUALITY, column, Bytes.fromBoolean(false), null);
+        } else {
+          return none(column);
+        }
+      }
+      case LESS_EQUAL: {
+        // b <= true  -> b IS NOT NULL
+        // b <= false -> b = false
+        if (value) {
+          return newIsNotNullPredicate(column);
+        } else {
+          return new KuduPredicate(PredicateType.EQUALITY, column, Bytes.fromBoolean(false), null);
+        }
+      }
+      default: throw new RuntimeException("unknown comparison op");
+    }
+  }
+
+  /**
+   * Creates a new comparison predicate on an integer or timestamp column.
+   * @param column the column schema
+   * @param op the comparison operation
+   * @param value the value to compare against
+   */
+  public static KuduPredicate newComparisonPredicate(ColumnSchema column,
+                                                     ComparisonOp op,
+                                                     long value) {
+    checkColumn(column, Type.INT8, Type.INT16, Type.INT32, Type.INT64, Type.TIMESTAMP);
+    Preconditions.checkArgument(value <= maxIntValue(column.getType()) &&
+                                    value >= minIntValue(column.getType()),
+                                "integer value out of range for %s column: %s",
+                                column.getType(), value);
+
+    if (op == ComparisonOp.LESS_EQUAL) {
+      if (value == maxIntValue(column.getType())) {
+        // If the value can't be incremented because it is at the top end of the
+        // range, then substitute the predicate with an IS NOT NULL predicate.
+        // This has the same effect as an inclusive upper bound on the maximum
+        // value. If the column is not nullable then the IS NOT NULL predicate
+        // is ignored.
+        return newIsNotNullPredicate(column);
+      }
+      value += 1;
+    } else if (op == ComparisonOp.GREATER) {
+      if (value == maxIntValue(column.getType())) {
+        return none(column);
+      }
+      value += 1;
+    }
+
+    byte[] bytes;
+    switch (column.getType()) {
+      case INT8: {
+        bytes = new byte[] { (byte) value };
+        break;
+      }
+      case INT16: {
+        bytes = Bytes.fromShort((short) value);
+        break;
+      }
+      case INT32: {
+        bytes = Bytes.fromInt((int) value);
+        break;
+      }
+      case INT64:
+      case TIMESTAMP: {
+        bytes = Bytes.fromLong(value);
+        break;
+      }
+      default: throw new RuntimeException("already checked");
+    }
+    switch (op) {
+      case GREATER:
+      case GREATER_EQUAL: return new KuduPredicate(PredicateType.RANGE, column, bytes, null);
+      case EQUAL: return new KuduPredicate(PredicateType.EQUALITY, column, bytes, null);
+      case LESS:
+      case LESS_EQUAL: return new KuduPredicate(PredicateType.RANGE, column, null, bytes);
+      default: throw new RuntimeException("unknown comparison op");
+    }
+  }
+
+  /**
+   * Creates a new comparison predicate on a float column.
+   * @param column the column schema
+   * @param op the comparison operation
+   * @param value the value to compare against
+   */
+  public static KuduPredicate newComparisonPredicate(ColumnSchema column,
+                                                     ComparisonOp op,
+                                                     float value) {
+    checkColumn(column, Type.FLOAT);
+    if (op == ComparisonOp.LESS_EQUAL) {
+      if (value == Float.POSITIVE_INFINITY) {
+        return newIsNotNullPredicate(column);
+      }
+      value = Math.nextAfter(value, Float.POSITIVE_INFINITY);
+    } else if (op == ComparisonOp.GREATER) {
+      if (value == Float.POSITIVE_INFINITY) {
+        return none(column);
+      }
+      value = Math.nextAfter(value, Float.POSITIVE_INFINITY);
+    }
+
+    byte[] bytes = Bytes.fromFloat(value);
+    switch (op) {
+      case GREATER:
+      case GREATER_EQUAL: return new KuduPredicate(PredicateType.RANGE, column, bytes, null);
+      case EQUAL: return new KuduPredicate(PredicateType.EQUALITY, column, bytes, null);
+      case LESS:
+      case LESS_EQUAL: return new KuduPredicate(PredicateType.RANGE, column, null, bytes);
+      default: throw new RuntimeException("unknown comparison op");
+    }
+  }
+
+  /**
+   * Creates a new comparison predicate on a double column.
+   * @param column the column schema
+   * @param op the comparison operation
+   * @param value the value to compare against
+   */
+  public static KuduPredicate newComparisonPredicate(ColumnSchema column,
+                                                     ComparisonOp op,
+                                                     double value) {
+    checkColumn(column, Type.DOUBLE);
+    if (op == ComparisonOp.LESS_EQUAL) {
+      if (value == Double.POSITIVE_INFINITY) {
+        return newIsNotNullPredicate(column);
+      }
+      value = Math.nextAfter(value, Double.POSITIVE_INFINITY);
+    } else if (op == ComparisonOp.GREATER) {
+      if (value == Double.POSITIVE_INFINITY) {
+        return none(column);
+      }
+      value = Math.nextAfter(value, Double.POSITIVE_INFINITY);
+    }
+
+    byte[] bytes = Bytes.fromDouble(value);
+    switch (op) {
+      case GREATER:
+      case GREATER_EQUAL: return new KuduPredicate(PredicateType.RANGE, column, bytes, null);
+      case EQUAL: return new KuduPredicate(PredicateType.EQUALITY, column, bytes, null);
+      case LESS:
+      case LESS_EQUAL: return new KuduPredicate(PredicateType.RANGE, column, null, bytes);
+      default: throw new RuntimeException("unknown comparison op");
+    }
+  }
+
+  /**
+   * Creates a new comparison predicate on a string column.
+   * @param column the column schema
+   * @param op the comparison operation
+   * @param value the value to compare against
+   */
+  public static KuduPredicate newComparisonPredicate(ColumnSchema column,
+                                                     ComparisonOp op,
+                                                     String value) {
+    checkColumn(column, Type.STRING);
+
+    byte[] bytes = Bytes.fromString(value);
+    if (op == ComparisonOp.LESS_EQUAL || op == ComparisonOp.GREATER) {
+      bytes = Arrays.copyOf(bytes, bytes.length + 1);
+    }
+
+    switch (op) {
+      case GREATER:
+      case GREATER_EQUAL: return new KuduPredicate(PredicateType.RANGE, column, bytes, null);
+      case EQUAL: return new KuduPredicate(PredicateType.EQUALITY, column, bytes, null);
+      case LESS:
+      case LESS_EQUAL: return new KuduPredicate(PredicateType.RANGE, column, null, bytes);
+      default: throw new RuntimeException("unknown comparison op");
+    }
+  }
+
+  /**
+   * Creates a new comparison predicate on a binary column.
+   * @param column the column schema
+   * @param op the comparison operation
+   * @param value the value to compare against
+   */
+  public static KuduPredicate newComparisonPredicate(ColumnSchema column,
+                                                     ComparisonOp op,
+                                                     byte[] value) {
+    checkColumn(column, Type.BINARY);
+
+    if (op == ComparisonOp.LESS_EQUAL || op == ComparisonOp.GREATER) {
+      value = Arrays.copyOf(value, value.length + 1);
+    }
+
+    switch (op) {
+      case GREATER:
+      case GREATER_EQUAL: return new KuduPredicate(PredicateType.RANGE, column, value, null);
+      case EQUAL: return new KuduPredicate(PredicateType.EQUALITY, column, value, null);
+      case LESS:
+      case LESS_EQUAL: return new KuduPredicate(PredicateType.RANGE, column, null, value);
+      default: throw new RuntimeException("unknown comparison op");
+    }
+  }
+
+  /**
+   * @param type the predicate type
+   * @param column the column to which the predicate applies
+   * @param lower the lower bound serialized value if this is a Range predicate,
+   *              or the equality value if this is an Equality predicate
+   * @param upper the upper bound serialized value if this is an Equality predicate
+   */
+  @VisibleForTesting
+  KuduPredicate(PredicateType type, ColumnSchema column, byte[] lower, byte[] upper) {
+    this.type = type;
+    this.column = column;
+    this.lower = lower;
+    this.upper = upper;
+  }
+
+  /**
+   * Factory function for a {@code None} predicate.
+   * @param column the column to which the predicate applies
+   * @return a None predicate
+   */
+  @VisibleForTesting
+  static KuduPredicate none(ColumnSchema column) {
+    return new KuduPredicate(PredicateType.NONE, column, null, null);
+  }
+
+  /**
+   * Factory function for an {@code IS NOT NULL} predicate.
+   * @param column the column to which the predicate applies
+   * @return a {@code IS NOT NULL} predicate
+   */
+  @VisibleForTesting
+  static KuduPredicate newIsNotNullPredicate(ColumnSchema column) {
+    return new KuduPredicate(PredicateType.IS_NOT_NULL, column, null, null);
+  }
+
+  /**
+   * @return the type of this predicate
+   */
+  PredicateType getType() {
+    return type;
+  }
+
+  /**
+   * Merges another {@code ColumnPredicate} into this one, returning a new
+   * {@code ColumnPredicate} which matches the logical intersection ({@code AND})
+   * of the input predicates.
+   * @param other the predicate to merge with this predicate
+   * @return a new predicate that is the logical intersection
+   */
+  KuduPredicate merge(KuduPredicate other) {
+    Preconditions.checkArgument(column.equals(other.column),
+                                "predicates from different columns may not be merged");
+    if (type == PredicateType.NONE || other.type == PredicateType.NONE) {
+      return none(column);
+    }
+
+    if (type == PredicateType.IS_NOT_NULL) {
+      // NOT NULL is less selective than all other predicate types, so the
+      // intersection of NOT NULL with any other predicate is just the other
+      // predicate.
+      //
+      // Note: this will no longer be true when an IS NULL predicate type is
+      // added.
+      return other;
+    }
+
+    if (type == PredicateType.EQUALITY) {
+      if (other.type == PredicateType.EQUALITY) {
+        if (compare(lower, other.lower) != 0) {
+          return none(this.column);
+        } else {
+          return this;
+        }
+      } else {
+        if ((other.lower == null || compare(lower, other.lower) >= 0) &&
+            (other.upper == null || compare(lower, other.upper) < 0)) {
+          return this;
+        } else {
+          return none(this.column);
+        }
+      }
+    } else {
+      if (other.type == PredicateType.EQUALITY) {
+        return other.merge(this);
+      } else {
+        byte[] newLower = other.lower == null ||
+            (lower != null && compare(lower, other.lower) >= 0) ? lower : other.lower;
+        byte[] newUpper = other.upper == null ||
+            (upper != null && compare(upper, other.upper) <= 0) ? upper : other.upper;
+        if (newLower != null && newUpper != null && compare(newLower, newUpper) >= 0) {
+          return none(column);
+        } else {
+          if (newLower != null && newUpper != null && areConsecutive(newLower, newUpper)) {
+            return new KuduPredicate(PredicateType.EQUALITY, column, newLower, null);
+          } else {
+            return new KuduPredicate(PredicateType.RANGE, column, newLower, newUpper);
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * @return the schema of the predicate column
+   */
+  ColumnSchema getColumn() {
+    return column;
+  }
+
+  /**
+   * Convert the predicate to the protobuf representation.
+   * @return the protobuf message for this predicate.
+   */
+  Common.ColumnPredicatePB toPB() {
+    Common.ColumnPredicatePB.Builder builder = Common.ColumnPredicatePB.newBuilder();
+    builder.setColumn(column.getName());
+
+    switch (type) {
+      case EQUALITY: {
+        builder.getEqualityBuilder().setValue(ByteString.copyFrom(lower));
+        break;
+      }
+      case RANGE: {
+        Common.ColumnPredicatePB.Range.Builder b = builder.getRangeBuilder();
+        if (lower != null) {
+          b.setLower(ByteString.copyFrom(lower));
+        }
+        if (upper != null) {
+          b.setUpper(ByteString.copyFrom(upper));
+        }
+        break;
+      }
+      case IS_NOT_NULL: {
+        builder.setIsNotNull(builder.getIsNotNullBuilder());
+        break;
+      }
+      case NONE: throw new IllegalStateException(
+          "can not convert None predicate to protobuf message");
+      default: throw new IllegalArgumentException(
+          String.format("unknown predicate type: %s", type));
+    }
+    return builder.build();
+  }
+
+  /**
+   * Convert a column predicate protobuf message into a predicate.
+   * @return a predicate
+   */
+  static KuduPredicate fromPB(Schema schema, Common.ColumnPredicatePB pb) {
+    ColumnSchema column = schema.getColumn(pb.getColumn());
+    switch (pb.getPredicateCase()) {
+      case EQUALITY: {
+        return new KuduPredicate(PredicateType.EQUALITY, column,
+                                 pb.getEquality().getValue().toByteArray(), null);
+
+      }
+      case RANGE: {
+        Common.ColumnPredicatePB.Range range = pb.getRange();
+        return new KuduPredicate(PredicateType.RANGE, column,
+                                 range.hasLower() ? range.getLower().toByteArray() : null,
+                                 range.hasUpper() ? range.getUpper().toByteArray() : null);
+      }
+      default: throw new IllegalArgumentException("unknown predicate type");
+    }
+  }
+
+  /**
+   * Compares two bounds based on the type of this predicate's column.
+   * @param a the first serialized value
+   * @param b the second serialized value
+   * @return the comparison of the serialized values based on the column type
+   */
+  private int compare(byte[] a, byte[] b) {
+    switch (column.getType().getDataType()) {
+      case BOOL:
+        return Boolean.compare(Bytes.getBoolean(a), Bytes.getBoolean(b));
+      case INT8:
+        return Byte.compare(Bytes.getByte(a), Bytes.getByte(b));
+      case INT16:
+        return Short.compare(Bytes.getShort(a), Bytes.getShort(b));
+      case INT32:
+        return Integer.compare(Bytes.getInt(a), Bytes.getInt(b));
+      case INT64:
+      case TIMESTAMP:
+        return Long.compare(Bytes.getLong(a), Bytes.getLong(b));
+      case FLOAT:
+        return Float.compare(Bytes.getFloat(a), Bytes.getFloat(b));
+      case DOUBLE:
+        return Double.compare(Bytes.getDouble(a), Bytes.getDouble(b));
+      case STRING:
+      case BINARY:
+        return UnsignedBytes.lexicographicalComparator().compare(a, b);
+      default:
+        throw new IllegalStateException(String.format("unknown column type %s", column.getType()));
+    }
+  }
+
+  /**
+   * Returns true if increment(a) == b.
+   * @param a the value which would be incremented
+   * @param b the target value
+   * @return true if increment(a) == b
+   */
+  private boolean areConsecutive(byte[] a, byte[] b) {
+    switch (column.getType().getDataType()) {
+      case BOOL: return false;
+      case INT8: {
+        byte m = Bytes.getByte(a);
+        byte n = Bytes.getByte(b);
+        return m < n && m + 1 == n;
+      }
+      case INT16: {
+        short m = Bytes.getShort(a);
+        short n = Bytes.getShort(b);
+        return m < n && m + 1 == n;
+      }
+      case INT32: {
+        int m = Bytes.getInt(a);
+        int n = Bytes.getInt(b);
+        return m < n && m + 1 == n;
+      }
+      case INT64:
+      case TIMESTAMP: {
+        long m = Bytes.getLong(a);
+        long n = Bytes.getLong(b);
+        return m < n && m + 1 == n;
+      }
+      case FLOAT: {
+        float m = Bytes.getFloat(a);
+        float n = Bytes.getFloat(b);
+        return m < n && Math.nextAfter(m, Float.POSITIVE_INFINITY) == n;
+      }
+      case DOUBLE: {
+        double m = Bytes.getDouble(a);
+        double n = Bytes.getDouble(b);
+        return m < n && Math.nextAfter(m, Double.POSITIVE_INFINITY) == n;
+      }
+      case STRING:
+      case BINARY: {
+        if (a.length + 1 != b.length || b[a.length] != 0) return false;
+        for (int i = 0; i < a.length; i++) {
+          if (a[i] != b[i]) return false;
+        }
+        return true;
+      }
+      default:
+        throw new IllegalStateException(String.format("unknown column type %s", column.getType()));
+    }
+  }
+
+  /**
+   * Returns the maximum value for the integer type.
+   * @param type an integer type
+   * @return the maximum value
+   */
+  @VisibleForTesting
+  static long maxIntValue(Type type) {
+    switch (type) {
+      case INT8: return Byte.MAX_VALUE;
+      case INT16: return Short.MAX_VALUE;
+      case INT32: return Integer.MAX_VALUE;
+      case TIMESTAMP:
+      case INT64: return Long.MAX_VALUE;
+      default: throw new IllegalArgumentException("type must be an integer type");
+    }
+  }
+
+  /**
+   * Returns the minimum value for the integer type.
+   * @param type an integer type
+   * @return the minimum value
+   */
+  @VisibleForTesting
+  static long minIntValue(Type type) {
+    switch (type) {
+      case INT8: return Byte.MIN_VALUE;
+      case INT16: return Short.MIN_VALUE;
+      case INT32: return Integer.MIN_VALUE;
+      case TIMESTAMP:
+      case INT64: return Long.MIN_VALUE;
+      default: throw new IllegalArgumentException("type must be an integer type");
+    }
+  }
+
+
+  /**
+   * Checks that the column is one of the expected types.
+   * @param column the column being checked
+   * @param passedTypes the expected types (logical OR)
+   */
+  private static void checkColumn(ColumnSchema column, Type... passedTypes) {
+    for (Type type : passedTypes) {
+      if (column.getType().equals(type)) return;
+    }
+    throw new IllegalArgumentException(String.format("%s's type isn't %s, it's %s",
+                                                     column.getName(), Arrays.toString(passedTypes),
+                                                     column.getType().getName()));
+  }
+
+  /**
+   * Returns the string value of serialized value according to the type of column.
+   * @param value the value
+   * @return the text representation of the value
+   */
+  private String valueToString(byte[] value) {
+    switch (column.getType().getDataType()) {
+      case BOOL: return Boolean.toString(Bytes.getBoolean(value));
+      case INT8: return Byte.toString(Bytes.getByte(value));
+      case INT16: return Short.toString(Bytes.getShort(value));
+      case INT32: return Integer.toString(Bytes.getInt(value));
+      case INT64: return Long.toString(Bytes.getLong(value));
+      case TIMESTAMP: return RowResult.timestampToString(Bytes.getLong(value));
+      case FLOAT: return Float.toString(Bytes.getFloat(value));
+      case DOUBLE: return Double.toString(Bytes.getDouble(value));
+      case STRING: {
+        String v = Bytes.getString(value);
+        StringBuilder sb = new StringBuilder(2 + v.length());
+        sb.append('"');
+        sb.append(v);
+        sb.append('"');
+        return sb.toString();
+      }
+      case BINARY: return Bytes.hex(value);
+      default:
+        throw new IllegalStateException(String.format("unknown column type %s", column.getType()));
+    }
+  }
+
+  @Override
+  public String toString() {
+    switch (type) {
+      case EQUALITY: return String.format("`%s` = %s", column.getName(), valueToString(lower));
+      case RANGE: {
+        if (lower == null) {
+          return String.format("`%s` < %s", column.getName(), valueToString(upper));
+        } else if (upper == null) {
+          return String.format("`%s` >= %s", column.getName(), valueToString(lower));
+        } else {
+          return String.format("`%s` >= %s AND `%s` < %s",
+                               column.getName(), valueToString(lower),
+                               column.getName(), valueToString(upper));
+        }
+      }
+      case IS_NOT_NULL: return String.format("`%s` IS NOT NULL", column.getName());
+      case NONE: return String.format("`%s` NONE", column.getName());
+      default: throw new IllegalArgumentException(String.format("unknown predicate type %s", type));
+    }
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    KuduPredicate that = (KuduPredicate) o;
+    return type == that.type &&
+        column.equals(that.column) &&
+        Arrays.equals(lower, that.lower) &&
+        Arrays.equals(upper, that.upper);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(type, column, Arrays.hashCode(lower), Arrays.hashCode(upper));
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-kudu/blob/16c03cda/java/kudu-client/src/main/java/org/kududb/client/KuduScanner.java
----------------------------------------------------------------------
diff --git a/java/kudu-client/src/main/java/org/kududb/client/KuduScanner.java b/java/kudu-client/src/main/java/org/kududb/client/KuduScanner.java
index cdaaa46..4756279 100644
--- a/java/kudu-client/src/main/java/org/kududb/client/KuduScanner.java
+++ b/java/kudu-client/src/main/java/org/kududb/client/KuduScanner.java
@@ -130,7 +130,7 @@ public class KuduScanner {
     public KuduScanner build() {
       return new KuduScanner(new AsyncKuduScanner(
           client, table, projectedColumnNames, projectedColumnIndexes, readMode,
-          scanRequestTimeout, columnRangePredicates, limit, cacheBlocks,
+          scanRequestTimeout, predicates, limit, cacheBlocks,
           prefetching, lowerBoundPrimaryKey, upperBoundPrimaryKey,
           lowerBoundPartitionKey, upperBoundPartitionKey,
           htTimestamp, batchSizeBytes));

http://git-wip-us.apache.org/repos/asf/incubator-kudu/blob/16c03cda/java/kudu-client/src/main/java/org/kududb/client/ProtobufHelper.java
----------------------------------------------------------------------
diff --git a/java/kudu-client/src/main/java/org/kududb/client/ProtobufHelper.java b/java/kudu-client/src/main/java/org/kududb/client/ProtobufHelper.java
index 4f91bd2..2b2cf64 100644
--- a/java/kudu-client/src/main/java/org/kududb/client/ProtobufHelper.java
+++ b/java/kudu-client/src/main/java/org/kududb/client/ProtobufHelper.java
@@ -79,24 +79,27 @@ public class ProtobufHelper {
     return schemaBuilder.build();
   }
 
+  public static ColumnSchema pbToColumnSchema(Common.ColumnSchemaPB pb) {
+    Type type = Type.getTypeForDataType(pb.getType());
+    Object defaultValue = pb.hasReadDefaultValue() ?
+        byteStringToObject(type, pb.getReadDefaultValue()) : null;
+    ColumnSchema.Encoding encoding = ColumnSchema.Encoding.valueOf(pb.getEncoding().name());
+    ColumnSchema.CompressionAlgorithm compressionAlgorithm =
+        ColumnSchema.CompressionAlgorithm.valueOf(pb.getCompression().name());
+    return new ColumnSchema.ColumnSchemaBuilder(pb.getName(), type)
+                           .key(pb.getIsKey())
+                           .nullable(pb.getIsNullable())
+                           .defaultValue(defaultValue)
+                           .encoding(encoding)
+                           .compressionAlgorithm(compressionAlgorithm)
+                           .build();
+  }
+
   public static Schema pbToSchema(Common.SchemaPB schema) {
     List<ColumnSchema> columns = new ArrayList<>(schema.getColumnsCount());
     List<Integer> columnIds = new ArrayList<>(schema.getColumnsCount());
     for (Common.ColumnSchemaPB columnPb : schema.getColumnsList()) {
-      Type type = Type.getTypeForDataType(columnPb.getType());
-      Object defaultValue = columnPb.hasReadDefaultValue() ? byteStringToObject(type,
-          columnPb.getReadDefaultValue()) : null;
-      ColumnSchema.Encoding encoding = ColumnSchema.Encoding.valueOf(columnPb.getEncoding().name());
-      ColumnSchema.CompressionAlgorithm compressionAlgorithm =
-          ColumnSchema.CompressionAlgorithm.valueOf(columnPb.getCompression().name());
-      ColumnSchema column = new ColumnSchema.ColumnSchemaBuilder(columnPb.getName(), type)
-          .key(columnPb.getIsKey())
-          .nullable(columnPb.getIsNullable())
-          .defaultValue(defaultValue)
-          .encoding(encoding)
-          .compressionAlgorithm(compressionAlgorithm)
-          .build();
-      columns.add(column);
+      columns.add(pbToColumnSchema(columnPb));
       int id = columnPb.getId();
       if (id < 0) {
         throw new IllegalArgumentException("Illegal column ID: " + id);

http://git-wip-us.apache.org/repos/asf/incubator-kudu/blob/16c03cda/java/kudu-client/src/test/java/org/kududb/client/BaseKuduTest.java
----------------------------------------------------------------------
diff --git a/java/kudu-client/src/test/java/org/kududb/client/BaseKuduTest.java b/java/kudu-client/src/test/java/org/kududb/client/BaseKuduTest.java
index afb4e54..7014630 100644
--- a/java/kudu-client/src/test/java/org/kududb/client/BaseKuduTest.java
+++ b/java/kudu-client/src/test/java/org/kududb/client/BaseKuduTest.java
@@ -164,9 +164,14 @@ public class BaseKuduTest {
     return counter.get();
   }
 
-  protected List<String> scanTableToStrings(KuduTable table) throws Exception {
+  protected List<String> scanTableToStrings(KuduTable table,
+                                            KuduPredicate... predicates) throws Exception {
     List<String> rowStrings = Lists.newArrayList();
-    KuduScanner scanner = syncClient.newScannerBuilder(table).build();
+    KuduScanner.KuduScannerBuilder scanBuilder = syncClient.newScannerBuilder(table);
+    for (KuduPredicate predicate : predicates) {
+      scanBuilder.addPredicate(predicate);
+    }
+    KuduScanner scanner = scanBuilder.build();
     while (scanner.hasMoreRows()) {
       RowResultIterator rows = scanner.nextRows();
       for (RowResult r : rows) {

http://git-wip-us.apache.org/repos/asf/incubator-kudu/blob/16c03cda/java/kudu-client/src/test/java/org/kududb/client/TestBytes.java
----------------------------------------------------------------------
diff --git a/java/kudu-client/src/test/java/org/kududb/client/TestBytes.java b/java/kudu-client/src/test/java/org/kududb/client/TestBytes.java
index 57a93da..11c2035 100644
--- a/java/kudu-client/src/test/java/org/kududb/client/TestBytes.java
+++ b/java/kudu-client/src/test/java/org/kududb/client/TestBytes.java
@@ -18,6 +18,7 @@ package org.kududb.client;
 
 import static org.junit.Assert.assertEquals;
 
+import org.junit.Assert;
 import org.junit.Test;
 
 import java.math.BigInteger;
@@ -94,4 +95,11 @@ public class TestBytes {
     Bytes.setDouble(bytes, aDouble);
     assertEquals(aDouble, Bytes.getDouble(bytes), 0.001);
   }
+
+  @Test
+  public void testHex() {
+    byte[] bytes = new byte[] { (byte) 0x01, (byte) 0x23, (byte) 0x45, (byte) 0x67,
+                                (byte) 0x89, (byte) 0xAB, (byte) 0xCD, (byte) 0xEF };
+    Assert.assertEquals("0x0123456789ABCDEF", Bytes.hex(bytes));
+  }
 }

http://git-wip-us.apache.org/repos/asf/incubator-kudu/blob/16c03cda/java/kudu-client/src/test/java/org/kududb/client/TestKuduClient.java
----------------------------------------------------------------------
diff --git a/java/kudu-client/src/test/java/org/kududb/client/TestKuduClient.java b/java/kudu-client/src/test/java/org/kududb/client/TestKuduClient.java
index c99238c..7ff8a18 100644
--- a/java/kudu-client/src/test/java/org/kududb/client/TestKuduClient.java
+++ b/java/kudu-client/src/test/java/org/kududb/client/TestKuduClient.java
@@ -20,6 +20,10 @@ import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.kududb.client.KuduPredicate.ComparisonOp.GREATER;
+import static org.kududb.client.KuduPredicate.ComparisonOp.GREATER_EQUAL;
+import static org.kududb.client.KuduPredicate.ComparisonOp.LESS;
+import static org.kududb.client.KuduPredicate.ComparisonOp.LESS_EQUAL;
 import static org.kududb.client.RowResult.timestampToString;
 
 import java.util.ArrayList;
@@ -258,6 +262,53 @@ public class TestKuduClient extends BaseKuduTest {
   }
 
   /**
+   * Test scanning with predicates.
+   */
+  @Test
+  public void testScanWithPredicates() throws Exception {
+    Schema schema = createManyStringsSchema();
+    syncClient.createTable(tableName, schema);
+
+    KuduSession session = syncClient.newSession();
+    session.setFlushMode(SessionConfiguration.FlushMode.AUTO_FLUSH_BACKGROUND);
+    KuduTable table = syncClient.openTable(tableName);
+    for (int i = 0; i < 100; i++) {
+      Insert insert = table.newInsert();
+      PartialRow row = insert.getRow();
+      row.addString("key", String.format("key_%02d", i));
+      row.addString("c1", "c1_" + i);
+      row.addString("c2", "c2_" + i);
+      session.apply(insert);
+    }
+    session.flush();
+
+    assertEquals(100, scanTableToStrings(table).size());
+    assertEquals(50, scanTableToStrings(table,
+        KuduPredicate.newComparisonPredicate(schema.getColumn("key"), GREATER_EQUAL, "key_50")
+    ).size());
+    assertEquals(25, scanTableToStrings(table,
+        KuduPredicate.newComparisonPredicate(schema.getColumn("key"), GREATER, "key_74")
+    ).size());
+    assertEquals(25, scanTableToStrings(table,
+        KuduPredicate.newComparisonPredicate(schema.getColumn("key"), GREATER, "key_24"),
+        KuduPredicate.newComparisonPredicate(schema.getColumn("c1"), LESS_EQUAL, "c1_49")
+    ).size());
+    assertEquals(50, scanTableToStrings(table,
+        KuduPredicate.newComparisonPredicate(schema.getColumn("key"), GREATER, "key_24"),
+        KuduPredicate.newComparisonPredicate(schema.getColumn("key"), GREATER_EQUAL, "key_50")
+    ).size());
+    assertEquals(0, scanTableToStrings(table,
+        KuduPredicate.newComparisonPredicate(schema.getColumn("c1"), GREATER, "c1_30"),
+        KuduPredicate.newComparisonPredicate(schema.getColumn("c2"), LESS, "c2_20")
+    ).size());
+    assertEquals(0, scanTableToStrings(table,
+        // Short circuit scan
+        KuduPredicate.newComparisonPredicate(schema.getColumn("c2"), GREATER, "c2_30"),
+        KuduPredicate.newComparisonPredicate(schema.getColumn("c2"), LESS, "c2_20")
+    ).size());
+  }
+
+  /**
    * Creates a local client that we auto-close while buffering one row, then makes sure that after
    * closing that we can read the row.
    */

http://git-wip-us.apache.org/repos/asf/incubator-kudu/blob/16c03cda/java/kudu-client/src/test/java/org/kududb/client/TestKuduPredicate.java
----------------------------------------------------------------------
diff --git a/java/kudu-client/src/test/java/org/kududb/client/TestKuduPredicate.java b/java/kudu-client/src/test/java/org/kududb/client/TestKuduPredicate.java
new file mode 100644
index 0000000..4915a18
--- /dev/null
+++ b/java/kudu-client/src/test/java/org/kududb/client/TestKuduPredicate.java
@@ -0,0 +1,628 @@
+// 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.kududb.client;
+
+import com.google.common.base.Preconditions;
+import org.junit.Assert;
+import org.junit.Test;
+import org.kududb.ColumnSchema;
+import org.kududb.Type;
+
+import static org.kududb.client.KuduPredicate.ComparisonOp.EQUAL;
+import static org.kududb.client.KuduPredicate.ComparisonOp.GREATER;
+import static org.kududb.client.KuduPredicate.ComparisonOp.GREATER_EQUAL;
+import static org.kududb.client.KuduPredicate.ComparisonOp.LESS;
+import static org.kududb.client.KuduPredicate.ComparisonOp.LESS_EQUAL;
+import static org.kududb.client.KuduPredicate.PredicateType.RANGE;
+
+public class TestKuduPredicate {
+
+  private static final ColumnSchema boolCol =
+      new ColumnSchema.ColumnSchemaBuilder("bool", Type.BOOL).build();
+
+  private static final ColumnSchema byteCol =
+      new ColumnSchema.ColumnSchemaBuilder("byte", Type.INT8).build();
+
+  private static final ColumnSchema shortCol =
+      new ColumnSchema.ColumnSchemaBuilder("short", Type.INT16).build();
+
+  private static final ColumnSchema intCol =
+      new ColumnSchema.ColumnSchemaBuilder("int", Type.INT32).build();
+
+  private static final ColumnSchema longCol =
+      new ColumnSchema.ColumnSchemaBuilder("long", Type.INT64).build();
+
+  private static final ColumnSchema floatCol =
+      new ColumnSchema.ColumnSchemaBuilder("float", Type.FLOAT).build();
+
+  private static final ColumnSchema doubleCol =
+      new ColumnSchema.ColumnSchemaBuilder("double", Type.DOUBLE).build();
+
+  private static final ColumnSchema stringCol =
+      new ColumnSchema.ColumnSchemaBuilder("string", Type.STRING).build();
+
+  private static final ColumnSchema binaryCol =
+      new ColumnSchema.ColumnSchemaBuilder("binary", Type.BINARY).build();
+
+  private static KuduPredicate intRange(int lower, int upper) {
+    Preconditions.checkArgument(lower < upper);
+    return new KuduPredicate(RANGE, intCol, Bytes.fromInt(lower), Bytes.fromInt(upper));
+  }
+
+  private void testMerge(KuduPredicate a,
+                         KuduPredicate b,
+                         KuduPredicate expected) {
+
+    Assert.assertEquals(expected, a.merge(b));
+    Assert.assertEquals(expected, b.merge(a));
+  }
+
+  /**
+   * Tests merges on all types of integer predicates.
+   */
+  @Test
+  public void testMergeInt() {
+
+    // Equality + Equality
+
+    // |
+    // |
+    // =
+    // |
+    testMerge(KuduPredicate.newComparisonPredicate(intCol, EQUAL, 0),
+              KuduPredicate.newComparisonPredicate(intCol, EQUAL, 0),
+              KuduPredicate.newComparisonPredicate(intCol, EQUAL, 0));
+    // |
+    //  |
+    // =
+    // None
+    testMerge(KuduPredicate.newComparisonPredicate(intCol, EQUAL, 0),
+              KuduPredicate.newComparisonPredicate(intCol, EQUAL, 1),
+              KuduPredicate.none(intCol));
+
+    // Range + Equality
+
+    // [-------->
+    //      |
+    // =
+    //      |
+    testMerge(KuduPredicate.newComparisonPredicate(intCol, GREATER_EQUAL, 0),
+              KuduPredicate.newComparisonPredicate(intCol, EQUAL, 10),
+              KuduPredicate.newComparisonPredicate(intCol, EQUAL, 10));
+
+    //    [-------->
+    //  |
+    // =
+    // None
+    testMerge(KuduPredicate.newComparisonPredicate(intCol, GREATER_EQUAL, 10),
+              KuduPredicate.newComparisonPredicate(intCol, EQUAL, 0),
+              KuduPredicate.none(intCol));
+
+    // <--------)
+    //      |
+    // =
+    //      |
+    testMerge(KuduPredicate.newComparisonPredicate(intCol, LESS, 10),
+              KuduPredicate.newComparisonPredicate(intCol, EQUAL, 5),
+              KuduPredicate.newComparisonPredicate(intCol, EQUAL, 5));
+
+    // <--------)
+    //            |
+    // =
+    // None
+    testMerge(KuduPredicate.newComparisonPredicate(intCol, LESS, 0),
+              KuduPredicate.newComparisonPredicate(intCol, EQUAL, 10),
+              KuduPredicate.none(intCol));
+
+    // Unbounded Range + Unbounded Range
+
+    // [--------> AND
+    // [-------->
+    // =
+    // [-------->
+
+    testMerge(KuduPredicate.newComparisonPredicate(intCol, GREATER_EQUAL, 0),
+              KuduPredicate.newComparisonPredicate(intCol, GREATER_EQUAL, 0),
+              KuduPredicate.newComparisonPredicate(intCol, GREATER_EQUAL, 0));
+
+    // [--------> AND
+    //    [----->
+    // =
+    //    [----->
+    testMerge(KuduPredicate.newComparisonPredicate(intCol, GREATER_EQUAL, 0),
+              KuduPredicate.newComparisonPredicate(intCol, GREATER_EQUAL, 5),
+              KuduPredicate.newComparisonPredicate(intCol, GREATER_EQUAL, 5));
+
+    // <--------) AND
+    // <--------)
+    // =
+    // <--------)
+
+    testMerge(KuduPredicate.newComparisonPredicate(intCol, LESS, 0),
+              KuduPredicate.newComparisonPredicate(intCol, LESS, 0),
+              KuduPredicate.newComparisonPredicate(intCol, LESS, 0));
+
+    // <--------) AND
+    // <----)
+    // =
+    // <----)
+
+    testMerge(KuduPredicate.newComparisonPredicate(intCol, LESS, 0),
+              KuduPredicate.newComparisonPredicate(intCol, LESS, -10),
+              KuduPredicate.newComparisonPredicate(intCol, LESS, -10));
+
+    //    [--------> AND
+    // <-------)
+    // =
+    //    [----)
+    testMerge(KuduPredicate.newComparisonPredicate(intCol, GREATER_EQUAL, 0),
+              KuduPredicate.newComparisonPredicate(intCol, LESS, 10),
+              intRange(0, 10));
+
+    //     [-----> AND
+    // <----)
+    // =
+    //     |
+    testMerge(KuduPredicate.newComparisonPredicate(intCol, GREATER_EQUAL, 5),
+              KuduPredicate.newComparisonPredicate(intCol, LESS, 6),
+              KuduPredicate.newComparisonPredicate(intCol, EQUAL, 5));
+
+    //     [-----> AND
+    // <---)
+    // =
+    // None
+    testMerge(KuduPredicate.newComparisonPredicate(intCol, GREATER_EQUAL, 5),
+              KuduPredicate.newComparisonPredicate(intCol, LESS, 5),
+              KuduPredicate.none(intCol));
+
+    //       [-----> AND
+    // <---)
+    // =
+    // None
+    testMerge(KuduPredicate.newComparisonPredicate(intCol, GREATER_EQUAL, 5),
+              KuduPredicate.newComparisonPredicate(intCol, LESS, 3),
+              KuduPredicate.none(intCol));
+
+    // Range + Range
+
+    // [--------) AND
+    // [--------)
+    // =
+    // [--------)
+
+    testMerge(intRange(0, 10),
+              intRange(0, 10),
+              intRange(0, 10));
+
+    // [--------) AND
+    // [----)
+    // =
+    // [----)
+    testMerge(intRange(0, 10),
+              intRange(0, 5),
+              intRange(0, 5));
+
+    // [--------) AND
+    //   [----)
+    // =
+    //   [----)
+    testMerge(intRange(0, 10),
+              intRange(3, 8),
+              intRange(3, 8));
+
+    // [-----) AND
+    //   [------)
+    // =
+    //   [---)
+    testMerge(intRange(0, 8),
+              intRange(3, 10),
+              intRange(3, 8));
+    // [--) AND
+    //    [---)
+    // =
+    // None
+    testMerge(intRange(0, 5),
+              intRange(5, 10),
+              KuduPredicate.none(intCol));
+
+    // [--) AND
+    //       [---)
+    // =
+    // None
+    testMerge(intRange(0, 3),
+              intRange(5, 10),
+              KuduPredicate.none(intCol));
+
+    // Lower Bound + Range
+
+    // [------------>
+    //       [---)
+    // =
+    //       [---)
+    testMerge(KuduPredicate.newComparisonPredicate(intCol, GREATER_EQUAL, 0),
+              intRange(5, 10),
+              intRange(5, 10));
+
+    // [------------>
+    // [--------)
+    // =
+    // [--------)
+    testMerge(KuduPredicate.newComparisonPredicate(intCol, GREATER_EQUAL, 5),
+              intRange(5, 10),
+              intRange(5, 10));
+
+    //      [------------>
+    // [--------)
+    // =
+    //      [---)
+    testMerge(KuduPredicate.newComparisonPredicate(intCol, GREATER_EQUAL, 5),
+              intRange(0, 10),
+              intRange(5, 10));
+
+    //          [------->
+    // [-----)
+    // =
+    // None
+    testMerge(KuduPredicate.newComparisonPredicate(intCol, GREATER_EQUAL, 10),
+              intRange(0, 5),
+              KuduPredicate.none(intCol));
+
+    // Upper Bound + Range
+
+    // <------------)
+    //       [---)
+    // =
+    //       [---)
+    testMerge(KuduPredicate.newComparisonPredicate(intCol, LESS, 10),
+              intRange(3, 8),
+              intRange(3, 8));
+
+    // <------------)
+    //     [--------)
+    // =
+    //     [--------)
+    testMerge(KuduPredicate.newComparisonPredicate(intCol, LESS, 10),
+              intRange(5, 10),
+              intRange(5, 10));
+
+
+    // <------------)
+    //         [--------)
+    // =
+    //         [----)
+    testMerge(KuduPredicate.newComparisonPredicate(intCol, LESS, 5),
+              intRange(0, 10),
+              intRange(0, 5));
+
+    // Range + Equality
+
+    //   [---) AND
+    // |
+    // =
+    // None
+    testMerge(intRange(3, 5),
+              KuduPredicate.newComparisonPredicate(intCol, EQUAL, 1),
+              KuduPredicate.none(intCol));
+
+    // [---) AND
+    // |
+    // =
+    // |
+    testMerge(intRange(0, 5),
+              KuduPredicate.newComparisonPredicate(intCol, EQUAL, 0),
+              KuduPredicate.newComparisonPredicate(intCol, EQUAL, 0));
+
+    // [---) AND
+    //   |
+    // =
+    //   |
+    testMerge(intRange(0, 5),
+              KuduPredicate.newComparisonPredicate(intCol, EQUAL, 3),
+              KuduPredicate.newComparisonPredicate(intCol, EQUAL, 3));
+
+    // [---) AND
+    //     |
+    // =
+    // None
+    testMerge(intRange(0, 5),
+              KuduPredicate.newComparisonPredicate(intCol, EQUAL, 5),
+              KuduPredicate.none(intCol));
+
+    // [---) AND
+    //       |
+    // =
+    // None
+    testMerge(intRange(0, 5),
+              KuduPredicate.newComparisonPredicate(intCol, EQUAL, 7),
+              KuduPredicate.none(intCol));
+
+    // None
+
+    // None AND
+    // [---->
+    // =
+    // None
+    testMerge(KuduPredicate.none(intCol),
+              KuduPredicate.newComparisonPredicate(intCol, GREATER_EQUAL, 0),
+              KuduPredicate.none(intCol));
+    // None AND
+    // <----)
+    // =
+    // None
+    testMerge(KuduPredicate.none(intCol),
+              KuduPredicate.newComparisonPredicate(intCol, LESS, 0),
+              KuduPredicate.none(intCol));
+
+    // None AND
+    // [----)
+    // =
+    // None
+    testMerge(KuduPredicate.none(intCol),
+              intRange(3, 7),
+              KuduPredicate.none(intCol));
+
+    // None AND
+    //  |
+    // =
+    // None
+    testMerge(KuduPredicate.none(intCol),
+              KuduPredicate.newComparisonPredicate(intCol, EQUAL, 5),
+              KuduPredicate.none(intCol));
+
+    // None AND
+    // None
+    // =
+    // None
+    testMerge(KuduPredicate.none(intCol),
+              KuduPredicate.none(intCol),
+              KuduPredicate.none(intCol));
+  }
+
+  /**
+   * Tests tricky merges on a var length type.
+   */
+  @Test
+  public void testMergeString() {
+
+    //         [----->
+    //  <-----)
+    // =
+    // None
+    testMerge(KuduPredicate.newComparisonPredicate(stringCol, GREATER_EQUAL, "b\0"),
+              KuduPredicate.newComparisonPredicate(stringCol, LESS, "b"),
+              KuduPredicate.none(stringCol));
+
+    //        [----->
+    //  <-----)
+    // =
+    // None
+    testMerge(KuduPredicate.newComparisonPredicate(stringCol, GREATER_EQUAL, "b"),
+              KuduPredicate.newComparisonPredicate(stringCol, LESS, "b"),
+              KuduPredicate.none(stringCol));
+
+    //       [----->
+    //  <----)
+    // =
+    //       |
+    testMerge(KuduPredicate.newComparisonPredicate(stringCol, GREATER_EQUAL, "b"),
+              KuduPredicate.newComparisonPredicate(stringCol, LESS, "b\0"),
+              KuduPredicate.newComparisonPredicate(stringCol, EQUAL, "b"));
+
+    //     [----->
+    //  <-----)
+    // =
+    //     [--)
+    testMerge(KuduPredicate.newComparisonPredicate(stringCol, GREATER_EQUAL, "a"),
+              KuduPredicate.newComparisonPredicate(stringCol, LESS, "a\0\0"),
+              new KuduPredicate(RANGE, stringCol,
+                                Bytes.fromString("a"), Bytes.fromString("a\0\0")));
+  }
+
+  @Test
+  public void testBoolean() {
+
+    // b >= false
+    Assert.assertEquals(KuduPredicate.newIsNotNullPredicate(boolCol),
+                        KuduPredicate.newComparisonPredicate(boolCol, GREATER_EQUAL, false));
+    // b > false
+    Assert.assertEquals(KuduPredicate.newComparisonPredicate(boolCol, EQUAL, true),
+                        KuduPredicate.newComparisonPredicate(boolCol, GREATER, false));
+    // b = false
+    Assert.assertEquals(KuduPredicate.newComparisonPredicate(boolCol, EQUAL, false),
+                        KuduPredicate.newComparisonPredicate(boolCol, EQUAL, false));
+    // b < false
+    Assert.assertEquals(KuduPredicate.none(boolCol),
+                        KuduPredicate.newComparisonPredicate(boolCol, LESS, false));
+    // b <= false
+    Assert.assertEquals(KuduPredicate.newComparisonPredicate(boolCol, EQUAL, false),
+                        KuduPredicate.newComparisonPredicate(boolCol, LESS_EQUAL, false));
+
+    // b >= true
+    Assert.assertEquals(KuduPredicate.newComparisonPredicate(boolCol, EQUAL, true),
+                        KuduPredicate.newComparisonPredicate(boolCol, GREATER_EQUAL, true));
+    // b > true
+    Assert.assertEquals(KuduPredicate.none(boolCol),
+                        KuduPredicate.newComparisonPredicate(boolCol, GREATER, true));
+    // b = true
+    Assert.assertEquals(KuduPredicate.newComparisonPredicate(boolCol, EQUAL, true),
+                        KuduPredicate.newComparisonPredicate(boolCol, EQUAL, true));
+    // b < true
+    Assert.assertEquals(KuduPredicate.newComparisonPredicate(boolCol, EQUAL, false),
+                        KuduPredicate.newComparisonPredicate(boolCol, LESS, true));
+    // b <= true
+    Assert.assertEquals(KuduPredicate.newIsNotNullPredicate(boolCol),
+                        KuduPredicate.newComparisonPredicate(boolCol, LESS_EQUAL, true));
+  }
+
+  /**
+   * Tests basic predicate merges across all types.
+   */
+  @Test
+  public void testAllTypesMerge() {
+
+    testMerge(KuduPredicate.newComparisonPredicate(boolCol, GREATER_EQUAL, false),
+              KuduPredicate.newComparisonPredicate(boolCol, LESS, true),
+              new KuduPredicate(KuduPredicate.PredicateType.EQUALITY,
+                                boolCol,
+                                Bytes.fromBoolean(false),
+                                null));
+
+    testMerge(KuduPredicate.newComparisonPredicate(boolCol, GREATER_EQUAL, false),
+              KuduPredicate.newComparisonPredicate(boolCol, LESS_EQUAL, true),
+              KuduPredicate.newIsNotNullPredicate(boolCol));
+
+    testMerge(KuduPredicate.newComparisonPredicate(byteCol, GREATER_EQUAL, 0),
+              KuduPredicate.newComparisonPredicate(byteCol, LESS, 10),
+              new KuduPredicate(RANGE,
+                                byteCol,
+                                new byte[] { (byte) 0 },
+                                new byte[] { (byte) 10 }));
+
+    testMerge(KuduPredicate.newComparisonPredicate(shortCol, GREATER_EQUAL, 0),
+              KuduPredicate.newComparisonPredicate(shortCol, LESS, 10),
+              new KuduPredicate(RANGE,
+                                shortCol,
+                                Bytes.fromShort((short) 0),
+                                Bytes.fromShort((short) 10)));
+
+    testMerge(KuduPredicate.newComparisonPredicate(longCol, GREATER_EQUAL, 0),
+              KuduPredicate.newComparisonPredicate(longCol, LESS, 10),
+              new KuduPredicate(RANGE,
+                                longCol,
+                                Bytes.fromLong(0),
+                                Bytes.fromLong(10)));
+
+    testMerge(KuduPredicate.newComparisonPredicate(floatCol, GREATER_EQUAL, 123.45f),
+              KuduPredicate.newComparisonPredicate(floatCol, LESS, 678.90f),
+              new KuduPredicate(RANGE,
+                                floatCol,
+                                Bytes.fromFloat(123.45f),
+                                Bytes.fromFloat(678.90f)));
+
+    testMerge(KuduPredicate.newComparisonPredicate(doubleCol, GREATER_EQUAL, 123.45),
+              KuduPredicate.newComparisonPredicate(doubleCol, LESS, 678.90),
+              new KuduPredicate(RANGE,
+                                doubleCol,
+                                Bytes.fromDouble(123.45),
+                                Bytes.fromDouble(678.90)));
+
+    testMerge(KuduPredicate.newComparisonPredicate(binaryCol, GREATER_EQUAL,
+                                                   new byte[] { 0, 1, 2, 3, 4, 5, 6 }),
+              KuduPredicate.newComparisonPredicate(binaryCol, LESS, new byte[] { 10 }),
+              new KuduPredicate(RANGE,
+                                binaryCol,
+                                new byte[] { 0, 1, 2, 3, 4, 5, 6 },
+                                new byte[] { 10 }));
+  }
+
+  @Test
+  public void testLessEqual() {
+    Assert.assertEquals(KuduPredicate.newComparisonPredicate(byteCol, LESS_EQUAL, 10),
+                        KuduPredicate.newComparisonPredicate(byteCol, LESS, 11));
+    Assert.assertEquals(KuduPredicate.newComparisonPredicate(shortCol, LESS_EQUAL, 10),
+                        KuduPredicate.newComparisonPredicate(shortCol, LESS, 11));
+    Assert.assertEquals(KuduPredicate.newComparisonPredicate(intCol, LESS_EQUAL, 10),
+                        KuduPredicate.newComparisonPredicate(intCol, LESS, 11));
+    Assert.assertEquals(KuduPredicate.newComparisonPredicate(longCol, LESS_EQUAL, 10),
+                        KuduPredicate.newComparisonPredicate(longCol, LESS, 11));
+    Assert.assertEquals(KuduPredicate.newComparisonPredicate(floatCol, LESS_EQUAL, 12.345f),
+                        KuduPredicate.newComparisonPredicate(floatCol, LESS, Math.nextAfter(12.345f, Float.POSITIVE_INFINITY)));
+    Assert.assertEquals(KuduPredicate.newComparisonPredicate(doubleCol, LESS_EQUAL, 12.345),
+                        KuduPredicate.newComparisonPredicate(doubleCol, LESS, Math.nextAfter(12.345, Float.POSITIVE_INFINITY)));
+    Assert.assertEquals(KuduPredicate.newComparisonPredicate(stringCol, LESS_EQUAL, "a"),
+                        KuduPredicate.newComparisonPredicate(stringCol, LESS, "a\0"));
+    Assert.assertEquals(KuduPredicate.newComparisonPredicate(binaryCol, LESS_EQUAL, new byte[] { (byte) 10 }),
+                        KuduPredicate.newComparisonPredicate(binaryCol, LESS, new byte[] { (byte) 10, (byte) 0 }));
+
+    Assert.assertEquals(KuduPredicate.newComparisonPredicate(byteCol, LESS_EQUAL, Byte.MAX_VALUE),
+                        KuduPredicate.newIsNotNullPredicate(byteCol));
+    Assert.assertEquals(KuduPredicate.newComparisonPredicate(shortCol, LESS_EQUAL, Short.MAX_VALUE),
+                        KuduPredicate.newIsNotNullPredicate(shortCol));
+    Assert.assertEquals(KuduPredicate.newComparisonPredicate(intCol, LESS_EQUAL, Integer.MAX_VALUE),
+                        KuduPredicate.newIsNotNullPredicate(intCol));
+    Assert.assertEquals(KuduPredicate.newComparisonPredicate(longCol, LESS_EQUAL, Long.MAX_VALUE),
+                        KuduPredicate.newIsNotNullPredicate(longCol));
+    Assert.assertEquals(KuduPredicate.newComparisonPredicate(floatCol, LESS_EQUAL, Float.MAX_VALUE),
+                        KuduPredicate.newComparisonPredicate(floatCol, LESS, Float.POSITIVE_INFINITY));
+    Assert.assertEquals(KuduPredicate.newComparisonPredicate(floatCol, LESS_EQUAL, Float.POSITIVE_INFINITY),
+                        KuduPredicate.newIsNotNullPredicate(floatCol));
+    Assert.assertEquals(KuduPredicate.newComparisonPredicate(doubleCol, LESS_EQUAL, Double.MAX_VALUE),
+                        KuduPredicate.newComparisonPredicate(doubleCol, LESS, Double.POSITIVE_INFINITY));
+    Assert.assertEquals(KuduPredicate.newComparisonPredicate(doubleCol, LESS_EQUAL, Double.POSITIVE_INFINITY),
+                        KuduPredicate.newIsNotNullPredicate(doubleCol));
+  }
+
+  @Test
+  public void testGreater() {
+    Assert.assertEquals(KuduPredicate.newComparisonPredicate(byteCol, GREATER_EQUAL, 11),
+                        KuduPredicate.newComparisonPredicate(byteCol, GREATER, 10));
+    Assert.assertEquals(KuduPredicate.newComparisonPredicate(shortCol, GREATER_EQUAL, 11),
+                        KuduPredicate.newComparisonPredicate(shortCol, GREATER, 10));
+    Assert.assertEquals(KuduPredicate.newComparisonPredicate(intCol, GREATER_EQUAL, 11),
+                        KuduPredicate.newComparisonPredicate(intCol, GREATER, 10));
+    Assert.assertEquals(KuduPredicate.newComparisonPredicate(longCol, GREATER_EQUAL, 11),
+                        KuduPredicate.newComparisonPredicate(longCol, GREATER, 10));
+    Assert.assertEquals(KuduPredicate.newComparisonPredicate(floatCol, GREATER_EQUAL, Math.nextAfter(12.345f, Float.MAX_VALUE)),
+                        KuduPredicate.newComparisonPredicate(floatCol, GREATER, 12.345f));
+    Assert.assertEquals(KuduPredicate.newComparisonPredicate(doubleCol, GREATER_EQUAL, Math.nextAfter(12.345, Float.MAX_VALUE)),
+                        KuduPredicate.newComparisonPredicate(doubleCol, GREATER, 12.345));
+    Assert.assertEquals(KuduPredicate.newComparisonPredicate(stringCol, GREATER_EQUAL, "a\0"),
+                        KuduPredicate.newComparisonPredicate(stringCol, GREATER, "a"));
+    Assert.assertEquals(KuduPredicate.newComparisonPredicate(binaryCol, GREATER_EQUAL, new byte[] { (byte) 10, (byte) 0 }),
+                        KuduPredicate.newComparisonPredicate(binaryCol, GREATER, new byte[] { (byte) 10 }));
+
+    Assert.assertEquals(KuduPredicate.none(byteCol),
+                        KuduPredicate.newComparisonPredicate(byteCol, GREATER, Byte.MAX_VALUE));
+    Assert.assertEquals(KuduPredicate.none(shortCol),
+                        KuduPredicate.newComparisonPredicate(shortCol, GREATER, Short.MAX_VALUE));
+    Assert.assertEquals(KuduPredicate.none(intCol),
+                        KuduPredicate.newComparisonPredicate(intCol, GREATER, Integer.MAX_VALUE));
+    Assert.assertEquals(KuduPredicate.none(longCol),
+                        KuduPredicate.newComparisonPredicate(longCol, GREATER, Long.MAX_VALUE));
+    Assert.assertEquals(KuduPredicate.newComparisonPredicate(floatCol, GREATER_EQUAL, Float.POSITIVE_INFINITY),
+                        KuduPredicate.newComparisonPredicate(floatCol, GREATER, Float.MAX_VALUE));
+    Assert.assertEquals(KuduPredicate.none(floatCol),
+                        KuduPredicate.newComparisonPredicate(floatCol, GREATER, Float.POSITIVE_INFINITY));
+    Assert.assertEquals(KuduPredicate.newComparisonPredicate(doubleCol, GREATER_EQUAL, Double.POSITIVE_INFINITY),
+                        KuduPredicate.newComparisonPredicate(doubleCol, GREATER, Double.MAX_VALUE));
+    Assert.assertEquals(KuduPredicate.none(doubleCol),
+                        KuduPredicate.newComparisonPredicate(doubleCol, GREATER, Double.POSITIVE_INFINITY));
+  }
+
+  @Test
+  public void testToString() {
+    Assert.assertEquals("`bool` = true",
+                        KuduPredicate.newComparisonPredicate(boolCol, EQUAL, true).toString());
+    Assert.assertEquals("`byte` = 11",
+                        KuduPredicate.newComparisonPredicate(byteCol, EQUAL, 11).toString());
+    Assert.assertEquals("`short` = 11",
+                        KuduPredicate.newComparisonPredicate(shortCol, EQUAL, 11).toString());
+    Assert.assertEquals("`int` = -123",
+                        KuduPredicate.newComparisonPredicate(intCol, EQUAL, -123).toString());
+    Assert.assertEquals("`long` = 5454",
+                        KuduPredicate.newComparisonPredicate(longCol, EQUAL, 5454).toString());
+    Assert.assertEquals("`float` = 123.456",
+                        KuduPredicate.newComparisonPredicate(floatCol, EQUAL, 123.456f).toString());
+    Assert.assertEquals("`double` = 123.456",
+                        KuduPredicate.newComparisonPredicate(doubleCol, EQUAL, 123.456).toString());
+    Assert.assertEquals("`string` = \"my string\"",
+                        KuduPredicate.newComparisonPredicate(stringCol, EQUAL, "my string").toString());
+    Assert.assertEquals("`binary` = 0xAB01CD", KuduPredicate.newComparisonPredicate(
+        binaryCol, EQUAL, new byte[] { (byte) 0xAB, (byte) 0x01, (byte) 0xCD }).toString());
+  }
+}


Mime
View raw message