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")); + } +}
