rowBuilder = new DefaultRowAdapter().createRowBuilder();
- merger = new RowMerger<>(rowBuilder);
+ merger = new RowMerger<>(rowBuilder, false);
}
@Override
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/RowSetUtil.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/RowSetUtil.java
index fbc19ad4bc..68f81cc56f 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/RowSetUtil.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/RowSetUtil.java
@@ -50,80 +50,79 @@ public final class RowSetUtil {
private RowSetUtil() {}
/**
- * Splits the provided {@link RowSet} along the provided splitPoint into 2 segments. The right
- * segment will contain all keys that are strictly greater than the splitPoint and all {@link
- * RowRange}s truncated to start right after the splitPoint. The primary usecase is to resume a
- * broken ReadRows stream.
+ * Removes all the keys and range parts that fall on or before the splitPoint.
+ *
+ * The direction of before is determined by fromStart: for forward scans fromStart is true and
+ * will remove all the keys and range segments that would've been read prior to the splitPoint
+ * (ie. all of the keys sort lexiographically at or before the split point. For reverse scans,
+ * fromStart is false and all segments that sort lexiographically at or after the split point are
+ * removed. The primary usecase is to resume a broken ReadRows stream.
*/
- @Nonnull
- public static Split split(@Nonnull RowSet rowSet, @Nonnull ByteString splitPoint) {
- // Edgecase: splitPoint is the leftmost key ("")
- if (splitPoint.isEmpty()) {
- return Split.of(null, rowSet);
- }
+ public static RowSet erase(RowSet rowSet, ByteString splitPoint, boolean fromStart) {
+ RowSet.Builder newRowSet = RowSet.newBuilder();
- // An empty RowSet represents a full table scan. Make that explicit so that there is RowRange to
- // split.
if (rowSet.getRowKeysList().isEmpty() && rowSet.getRowRangesList().isEmpty()) {
rowSet = RowSet.newBuilder().addRowRanges(RowRange.getDefaultInstance()).build();
}
- RowSet.Builder leftBuilder = RowSet.newBuilder();
- boolean leftIsEmpty = true;
- RowSet.Builder rightBuilder = RowSet.newBuilder();
- boolean rightIsEmpty = true;
-
+ // Handle point lookups
for (ByteString key : rowSet.getRowKeysList()) {
- if (ByteStringComparator.INSTANCE.compare(key, splitPoint) <= 0) {
- leftBuilder.addRowKeys(key);
- leftIsEmpty = false;
+ if (fromStart) {
+ // key is right of the split
+ if (ByteStringComparator.INSTANCE.compare(key, splitPoint) > 0) {
+ newRowSet.addRowKeys(key);
+ }
} else {
- rightBuilder.addRowKeys(key);
- rightIsEmpty = false;
+ // key is left of the split
+ if (ByteStringComparator.INSTANCE.compare(key, splitPoint) < 0) {
+ newRowSet.addRowKeys(key);
+ }
}
}
- for (RowRange range : rowSet.getRowRangesList()) {
- StartPoint startPoint = StartPoint.extract(range);
- int startCmp =
- ComparisonChain.start()
- .compare(startPoint.value, splitPoint, ByteStringComparator.INSTANCE)
- // when value lies on the split point, only closed start points are on the left
- .compareTrueFirst(startPoint.isClosed, true)
- .result();
-
- // Range is fully on the right side
- if (startCmp > 0) {
- rightBuilder.addRowRanges(range);
- rightIsEmpty = false;
- continue;
+ // Handle ranges
+ for (RowRange rowRange : rowSet.getRowRangesList()) {
+ RowRange newRange = truncateRange(rowRange, splitPoint, fromStart);
+ if (newRange != null) {
+ newRowSet.addRowRanges(newRange);
}
+ }
- EndPoint endPoint = EndPoint.extract(range);
- int endCmp =
- ComparisonChain.start()
- // empty (true) end key means rightmost regardless of the split point
- .compareFalseFirst(endPoint.value.isEmpty(), false)
- .compare(endPoint.value, splitPoint, ByteStringComparator.INSTANCE)
- // don't care if the endpoint is open/closed: both will be on the left if the value is
- // <=
- .result();
-
- if (endCmp <= 0) {
- // Range is fully on the left
- leftBuilder.addRowRanges(range);
- leftIsEmpty = false;
- } else {
- // Range is split
- leftBuilder.addRowRanges(range.toBuilder().setEndKeyClosed(splitPoint));
- leftIsEmpty = false;
- rightBuilder.addRowRanges(range.toBuilder().setStartKeyOpen(splitPoint));
- rightIsEmpty = false;
+ // Return the new rowset if there is anything left to read
+ RowSet result = newRowSet.build();
+ if (result.getRowKeysList().isEmpty() && result.getRowRangesList().isEmpty()) {
+ return null;
+ }
+ return result;
+ }
+
+ private static RowRange truncateRange(RowRange range, ByteString split, boolean fromStart) {
+ if (fromStart) {
+ // range end is on or left of the split: skip
+ if (EndPoint.extract(range).compareTo(new EndPoint(split, true)) <= 0) {
+ return null;
+ }
+ } else {
+ // range is on or right of the split
+ if (StartPoint.extract(range).compareTo(new StartPoint(split, true)) >= 0) {
+ return null;
+ }
+ }
+ RowRange.Builder newRange = range.toBuilder();
+
+ if (fromStart) {
+ // range start is on or left of the split
+ if (StartPoint.extract(range).compareTo(new StartPoint(split, true)) <= 0) {
+ newRange.setStartKeyOpen(split);
+ }
+ } else {
+ // range end is on or right of the split
+ if (EndPoint.extract(range).compareTo(new EndPoint(split, true)) >= 0) {
+ newRange.setEndKeyOpen(split);
}
}
- return Split.of(
- leftIsEmpty ? null : leftBuilder.build(), rightIsEmpty ? null : rightBuilder.build());
+ return newRange.build();
}
/**
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/Query.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/Query.java
index 271ffe3adf..7de167dd52 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/Query.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/models/Query.java
@@ -184,6 +184,26 @@ public Query limit(long limit) {
return this;
}
+ /**
+ * Return rows in reverse order.
+ *
+ *
The row will be streamed in reverse lexiographic order of the keys. The row key ranges are
+ * still expected to be oriented the same way as forwards. ie [a,c] where a <= c. The row content
+ * will remain unchanged from the ordering forward scans. This is particularly useful to get the
+ * last N records before a key:
+ *
+ *
{@code
+ * query
+ * .range(ByteStringRange.unbounded().endOpen("key"))
+ * .limit(10)
+ * .reversed(true)
+ * }
+ */
+ public Query reversed(boolean enable) {
+ builder.setReversed(enable);
+ return this;
+ }
+
/**
* Split this query into multiple queries that can be evenly distributed across Bigtable nodes and
* be run in parallel. This method takes the results from {@link
@@ -379,11 +399,12 @@ public boolean advance(@Nonnull ByteString lastSeenRowKey) {
// Split the row ranges / row keys. Return false if there's nothing
// left on the right of the split point.
- RowSetUtil.Split split = RowSetUtil.split(query.builder.getRows(), lastSeenRowKey);
- if (split.getRight() == null) {
+ RowSet remaining =
+ RowSetUtil.erase(query.builder.getRows(), lastSeenRowKey, !query.builder.getReversed());
+ if (remaining == null) {
return false;
}
- query.builder.setRows(split.getRight());
+ query.builder.setRows(remaining);
return true;
}
}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java
index 9e1ba64222..eba09a7464 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubSettings.java
@@ -732,7 +732,7 @@ private Builder() {
.setTotalTimeout(PRIME_REQUEST_TIMEOUT)
.build());
- featureFlags = FeatureFlags.newBuilder();
+ featureFlags = FeatureFlags.newBuilder().setReverseScans(true);
}
private Builder(EnhancedBigtableStubSettings settings) {
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/readrows/ReadRowsResumptionStrategy.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/readrows/ReadRowsResumptionStrategy.java
index ab312ec41c..2db46c0c29 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/readrows/ReadRowsResumptionStrategy.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/readrows/ReadRowsResumptionStrategy.java
@@ -85,7 +85,8 @@ public ReadRowsRequest getResumeRequest(ReadRowsRequest originalRequest) {
return originalRequest;
}
- RowSet remaining = RowSetUtil.split(originalRequest.getRows(), lastKey).getRight();
+ RowSet remaining =
+ RowSetUtil.erase(originalRequest.getRows(), lastKey, !originalRequest.getReversed());
// Edge case: retrying a fulfilled request.
// A fulfilled request is one that has had all of its row keys and ranges fulfilled, or if it
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/readrows/RowMerger.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/readrows/RowMerger.java
index 0b8ebfd90d..54edf57a31 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/readrows/RowMerger.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/readrows/RowMerger.java
@@ -61,8 +61,8 @@ public class RowMerger implements Reframer {
private final StateMachine stateMachine;
private Queue mergedRows;
- public RowMerger(RowBuilder rowBuilder) {
- stateMachine = new StateMachine<>(rowBuilder);
+ public RowMerger(RowBuilder rowBuilder, boolean reversed) {
+ stateMachine = new StateMachine<>(rowBuilder, reversed);
mergedRows = new ArrayDeque<>();
}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/readrows/RowMergingCallable.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/readrows/RowMergingCallable.java
index 04814dd781..6f48166200 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/readrows/RowMergingCallable.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/readrows/RowMergingCallable.java
@@ -49,7 +49,7 @@ public RowMergingCallable(
public void call(
ReadRowsRequest request, ResponseObserver responseObserver, ApiCallContext context) {
RowBuilder rowBuilder = rowAdapter.createRowBuilder();
- RowMerger merger = new RowMerger<>(rowBuilder);
+ RowMerger merger = new RowMerger<>(rowBuilder, request.getReversed());
ReframingResponseObserver innerObserver =
new ReframingResponseObserver<>(responseObserver, merger);
inner.call(request, innerObserver, context);
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/readrows/StateMachine.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/readrows/StateMachine.java
index b6b6db678f..6791679829 100644
--- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/readrows/StateMachine.java
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/stub/readrows/StateMachine.java
@@ -76,6 +76,7 @@
*/
final class StateMachine {
private final RowBuilder adapter;
+ private boolean reversed;
private State currentState;
private ByteString lastCompleteRowKey;
@@ -102,9 +103,11 @@ final class StateMachine {
* Initialize a new state machine that's ready for a new row.
*
* @param adapter The adapter that will build the final row.
+ * @param reversed
*/
- StateMachine(RowBuilder adapter) {
+ StateMachine(RowBuilder adapter, boolean reversed) {
this.adapter = adapter;
+ this.reversed = reversed;
reset();
}
@@ -261,9 +264,15 @@ State handleChunk(CellChunk chunk) {
validate(chunk.hasFamilyName(), "AWAITING_NEW_ROW: family missing");
validate(chunk.hasQualifier(), "AWAITING_NEW_ROW: qualifier missing");
if (lastCompleteRowKey != null) {
- validate(
- ByteStringComparator.INSTANCE.compare(lastCompleteRowKey, chunk.getRowKey()) < 0,
- "AWAITING_NEW_ROW: key must be strictly increasing");
+
+ int cmp = ByteStringComparator.INSTANCE.compare(lastCompleteRowKey, chunk.getRowKey());
+ String direction = "increasing";
+ if (reversed) {
+ cmp *= -1;
+ direction = "decreasing";
+ }
+
+ validate(cmp < 0, "AWAITING_NEW_ROW: key must be strictly " + direction);
}
rowKey = chunk.getRowKey();
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/BigtableDataClientFactoryTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/BigtableDataClientFactoryTest.java
index ebda860851..edcda45938 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/BigtableDataClientFactoryTest.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/BigtableDataClientFactoryTest.java
@@ -26,6 +26,7 @@
import com.google.api.gax.rpc.TransportChannelProvider;
import com.google.api.gax.rpc.WatchdogProvider;
import com.google.bigtable.v2.BigtableGrpc;
+import com.google.bigtable.v2.FeatureFlags;
import com.google.bigtable.v2.InstanceName;
import com.google.bigtable.v2.MutateRowRequest;
import com.google.bigtable.v2.MutateRowResponse;
@@ -36,8 +37,14 @@
import com.google.cloud.bigtable.data.v2.internal.NameUtil;
import com.google.cloud.bigtable.data.v2.models.RowMutation;
import com.google.common.base.Preconditions;
+import com.google.common.io.BaseEncoding;
import io.grpc.Attributes;
+import io.grpc.Metadata;
import io.grpc.Server;
+import io.grpc.ServerCall;
+import io.grpc.ServerCall.Listener;
+import io.grpc.ServerCallHandler;
+import io.grpc.ServerInterceptor;
import io.grpc.ServerTransportFilter;
import io.grpc.stub.StreamObserver;
import java.io.IOException;
@@ -78,12 +85,24 @@ public class BigtableDataClientFactoryTest {
private final BlockingQueue setUpAttributes = new LinkedBlockingDeque<>();
private final BlockingQueue terminateAttributes = new LinkedBlockingDeque<>();
+ private final BlockingQueue requestMetadata = new LinkedBlockingDeque<>();
@Before
public void setUp() throws IOException {
service = new FakeBigtableService();
server =
FakeServiceBuilder.create(service)
+ .intercept(
+ new ServerInterceptor() {
+ @Override
+ public Listener interceptCall(
+ ServerCall call,
+ Metadata headers,
+ ServerCallHandler next) {
+ requestMetadata.add(headers);
+ return next.startCall(call, headers);
+ }
+ })
.addTransportFilter(
new ServerTransportFilter() {
@Override
@@ -276,6 +295,24 @@ public void testCreateWithRefreshingChannel() throws Exception {
assertThat(terminateAttributes).hasSize(poolSize);
}
+ @Test
+ public void testFeatureFlags() throws Exception {
+ try (BigtableDataClientFactory factory = BigtableDataClientFactory.create(defaultSettings);
+ BigtableDataClient client = factory.createDefault()) {
+
+ requestMetadata.clear();
+ client.mutateRow(RowMutation.create("some-table", "some-key").deleteRow());
+ }
+
+ Metadata metadata = requestMetadata.take();
+ String encodedValue =
+ metadata.get(Metadata.Key.of("bigtable-features", Metadata.ASCII_STRING_MARSHALLER));
+ FeatureFlags featureFlags =
+ FeatureFlags.parseFrom(BaseEncoding.base64Url().decode(encodedValue));
+
+ assertThat(featureFlags.getReverseScans()).isTrue();
+ }
+
@Test
public void testBulkMutationFlowControllerConfigured() throws Exception {
BigtableDataSettings settings =
@@ -306,6 +343,7 @@ private static class FakeBigtableService extends BigtableGrpc.BigtableImplBase {
volatile MutateRowRequest lastRequest;
BlockingQueue readRowsRequests = new LinkedBlockingDeque<>();
BlockingQueue pingAndWarmRequests = new LinkedBlockingDeque<>();
+
private ApiFunction readRowsCallback =
new ApiFunction() {
@Override
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/RowSetUtilTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/RowSetUtilTest.java
index 37ec606103..39d3c62c22 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/RowSetUtilTest.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/internal/RowSetUtilTest.java
@@ -36,37 +36,41 @@ public class RowSetUtilTest {
@Test
public void testSplitFullScan() {
RowSet input = RowSet.getDefaultInstance();
- RowSetUtil.Split split = RowSetUtil.split(input, ByteString.copyFromUtf8("g"));
- assertThat(split.getLeft()).isEqualTo(parse("-g]"));
- assertThat(split.getRight()).isEqualTo(parse("(g-"));
+ RowSet right = RowSetUtil.erase(input, ByteString.copyFromUtf8("g"), true);
+ assertThat(right).isEqualTo(parse("(g-"));
+
+ RowSet left = RowSetUtil.erase(input, ByteString.copyFromUtf8("g"), false);
+ assertThat(left).isEqualTo(parse("-g)"));
}
@Test
public void testSplitAllLeft() {
- RowSet input = parse("a,c,(a1-c],[a2-c],(a3-c),[a4-c)");
- RowSetUtil.Split split = RowSetUtil.split(input, ByteString.copyFromUtf8("c"));
+ RowSet input = parse("a,(a1-c),[a2-c),(a3-c),[a4-c)");
+ RowSet left = RowSetUtil.erase(input, ByteString.copyFromUtf8("c"), false);
+ RowSet right = RowSetUtil.erase(input, ByteString.copyFromUtf8("c"), true);
- assertThat(split.getLeft()).isEqualTo(input);
- assertThat(split.getRight()).isNull();
+ assertThat(left).isEqualTo(input);
+ assertThat(right).isNull();
}
@Test
public void testSplitAllRight() {
RowSet input = parse("a1,c,(a-c],[a2-c],(a3-c),[a4-c)");
- RowSetUtil.Split split = RowSetUtil.split(input, ByteString.copyFromUtf8("a"));
- assertThat(split.getLeft()).isNull();
- assertThat(split.getRight()).isEqualTo(input);
+ assertThat(RowSetUtil.erase(input, ByteString.copyFromUtf8("a"), true)).isEqualTo(input);
+ assertThat(RowSetUtil.erase(input, ByteString.copyFromUtf8("a"), false)).isNull();
}
@Test
public void testSplit() {
- RowSet input = parse("a1,c,(a1-c],[a2-c],(a3-c),[a4-c)");
- RowSetUtil.Split split = RowSetUtil.split(input, ByteString.copyFromUtf8("b"));
+ RowSet input = parse("a1,c,(a1-c],[a2-c],(a3-c),[a4-c),[b-z],(b-y]");
+
+ RowSet before = RowSetUtil.erase(input, ByteString.copyFromUtf8("b"), false);
+ RowSet after = RowSetUtil.erase(input, ByteString.copyFromUtf8("b"), true);
- assertThat(split.getLeft()).isEqualTo(parse("a1,(a1-b],[a2-b],(a3-b],[a4-b]"));
- assertThat(split.getRight()).isEqualTo(parse("c,(b-c],(b-c],(b-c),(b-c)"));
+ assertThat(before).isEqualTo(parse("a1,(a1-b),[a2-b),(a3-b),[a4-b)"));
+ assertThat(after).isEqualTo(parse("c,(b-c],(b-c],(b-c),(b-c),(b-z],(b-y]"));
}
@Test
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/ReadIT.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/ReadIT.java
index d8626059fa..7b58e14f7c 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/ReadIT.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/ReadIT.java
@@ -16,6 +16,7 @@
package com.google.cloud.bigtable.data.v2.it;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
import com.google.api.core.ApiFuture;
import com.google.api.core.ApiFutureCallback;
@@ -31,6 +32,7 @@
import com.google.cloud.bigtable.data.v2.models.RowCell;
import com.google.cloud.bigtable.data.v2.models.RowMutation;
import com.google.cloud.bigtable.data.v2.models.RowMutationEntry;
+import com.google.cloud.bigtable.test_helpers.env.EmulatorEnv;
import com.google.cloud.bigtable.test_helpers.env.TestEnvRule;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
@@ -224,6 +226,88 @@ public void rangeQueries() {
.isEmpty();
}
+ @Test
+ public void reversed() {
+ assume()
+ .withMessage("reverse scans are not supported in the emulator")
+ .that(testEnvRule.env())
+ .isNotInstanceOf(EmulatorEnv.class);
+ BigtableDataClient client = testEnvRule.env().getDataClient();
+ String tableId = testEnvRule.env().getTableId();
+ String familyId = testEnvRule.env().getFamilyId();
+ String uniqueKey = prefix + "-rev-queries";
+ String keyA = uniqueKey + "-" + "a";
+ String keyB = uniqueKey + "-" + "b";
+ String keyC = uniqueKey + "-" + "c";
+
+ long timestampMicros = System.currentTimeMillis() * 1_000;
+
+ client.bulkMutateRows(
+ BulkMutation.create(tableId)
+ .add(RowMutationEntry.create(keyA).setCell(familyId, "", timestampMicros, "A"))
+ .add(RowMutationEntry.create(keyB).setCell(familyId, "", timestampMicros, "B"))
+ .add(RowMutationEntry.create(keyC).setCell(familyId, "", timestampMicros, "C")));
+
+ Row expectedRowA =
+ Row.create(
+ ByteString.copyFromUtf8(keyA),
+ ImmutableList.of(
+ RowCell.create(
+ testEnvRule.env().getFamilyId(),
+ ByteString.copyFromUtf8(""),
+ timestampMicros,
+ ImmutableList.of(),
+ ByteString.copyFromUtf8("A"))));
+
+ Row expectedRowB =
+ Row.create(
+ ByteString.copyFromUtf8(keyB),
+ ImmutableList.of(
+ RowCell.create(
+ testEnvRule.env().getFamilyId(),
+ ByteString.copyFromUtf8(""),
+ timestampMicros,
+ ImmutableList.of(),
+ ByteString.copyFromUtf8("B"))));
+ Row expectedRowC =
+ Row.create(
+ ByteString.copyFromUtf8(keyC),
+ ImmutableList.of(
+ RowCell.create(
+ testEnvRule.env().getFamilyId(),
+ ByteString.copyFromUtf8(""),
+ timestampMicros,
+ ImmutableList.of(),
+ ByteString.copyFromUtf8("C"))));
+
+ assertThat(
+ ImmutableList.copyOf(
+ client.readRows(
+ Query.create(tableId).reversed(true).range(ByteStringRange.prefix(uniqueKey)))))
+ .containsExactly(expectedRowC, expectedRowB, expectedRowA)
+ .inOrder();
+
+ assertThat(
+ ImmutableList.copyOf(
+ client.readRows(
+ Query.create(tableId)
+ .reversed(true)
+ .range(ByteStringRange.prefix(uniqueKey))
+ .limit(2))))
+ .containsExactly(expectedRowC, expectedRowB)
+ .inOrder();
+
+ assertThat(
+ ImmutableList.copyOf(
+ client.readRows(
+ Query.create(tableId)
+ .reversed(true)
+ .range(ByteStringRange.unbounded().endOpen(keyC))
+ .limit(2))))
+ .containsExactly(expectedRowB, expectedRowA)
+ .inOrder();
+ }
+
@Test
public void readSingleNonexistentAsyncCallback() throws Exception {
ApiFuture future =
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/QueryTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/QueryTest.java
index 655aeda688..93e5b1c92f 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/QueryTest.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/models/QueryTest.java
@@ -505,4 +505,11 @@ public void testQueryPaginatorEmptyTable() {
assertThat(queryPaginator.advance(ByteString.EMPTY)).isFalse();
}
+
+ @Test
+ public void testQueryReversed() {
+ Query query = Query.create(TABLE_ID).reversed(true);
+ assertThat(query.toProto(requestContext))
+ .isEqualTo(expectedProtoBuilder().setReversed(true).build());
+ }
}
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubTest.java
index c147c112e5..5e6e6fbe5d 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubTest.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/EnhancedBigtableStubTest.java
@@ -53,6 +53,7 @@
import com.google.cloud.bigtable.data.v2.models.RowMutationEntry;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Queues;
+import com.google.common.io.BaseEncoding;
import com.google.protobuf.ByteString;
import com.google.protobuf.BytesValue;
import com.google.protobuf.StringValue;
@@ -230,6 +231,20 @@ public void testBatchJwtAudience()
assertThat(parsed.getPayload().getAudience()).isEqualTo("https://bigtable.googleapis.com/");
}
+ @Test
+ public void testFeatureFlags() throws InterruptedException, IOException, ExecutionException {
+
+ enhancedBigtableStub.readRowCallable().futureCall(Query.create("fake-table")).get();
+ Metadata metadata = metadataInterceptor.headers.take();
+
+ String encodedFeatureFlags =
+ metadata.get(Key.of("bigtable-features", Metadata.ASCII_STRING_MARSHALLER));
+ FeatureFlags featureFlags =
+ FeatureFlags.parseFrom(BaseEncoding.base64Url().decode(encodedFeatureFlags));
+
+ assertThat(featureFlags.getReverseScans()).isTrue();
+ }
+
@Test
public void testCreateReadRowsCallable() throws InterruptedException {
ServerStreamingCallable streamingCallable =
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/readrows/StateMachineTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/readrows/StateMachineTest.java
index cbb5e7d80f..c98506eb41 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/readrows/StateMachineTest.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/stub/readrows/StateMachineTest.java
@@ -34,7 +34,7 @@ public class StateMachineTest {
@Before
public void setUp() throws Exception {
- stateMachine = new StateMachine<>(new DefaultRowAdapter().createRowBuilder());
+ stateMachine = new StateMachine<>(new DefaultRowAdapter().createRowBuilder(), false);
}
@Test
diff --git a/samples/snippets/src/main/java/com/example/bigtable/Reads.java b/samples/snippets/src/main/java/com/example/bigtable/Reads.java
index d68997c649..1bd5609f96 100644
--- a/samples/snippets/src/main/java/com/example/bigtable/Reads.java
+++ b/samples/snippets/src/main/java/com/example/bigtable/Reads.java
@@ -200,6 +200,37 @@ public static void readPrefix(String projectId, String instanceId, String tableI
}
// [END bigtable_reads_prefix]
+ // [START bigtable_reverse_scan]
+ public static void readRowsReversed() {
+ // TODO(developer): Replace these variables before running the sample.
+ String projectId = "my-project-id";
+ String instanceId = "my-instance-id";
+ String tableId = "mobile-time-series";
+ readRowsReversed(projectId, instanceId, tableId);
+ }
+
+ public static void readRowsReversed(String projectId, String instanceId, String tableId) {
+ // Initialize client that will be used to send requests. This client only needs to be created
+ // once, and can be reused for multiple requests. After completing all of your requests, call
+ // the "close" method on the client to safely clean up any remaining background resources.
+ try (BigtableDataClient dataClient = BigtableDataClient.create(projectId, instanceId)) {
+ Query query =
+ Query.create(tableId)
+ .reversed(true)
+ .limit(2)
+ .prefix("phone#4c410523")
+ .range("phone#5c10102", "phone#5c10103");
+ ServerStream rows = dataClient.readRows(query);
+ for (Row row : rows) {
+ printRow(row);
+ }
+ } catch (IOException e) {
+ System.out.println(
+ "Unable to initialize service client, as a network error occurred: \n" + e.toString());
+ }
+ }
+ // [END bigtable_reverse_scan]
+
// [START bigtable_reads_filter]
public static void readFilter() {
// TODO(developer): Replace these variables before running the sample.
diff --git a/samples/snippets/src/test/java/com/example/bigtable/ReadsTest.java b/samples/snippets/src/test/java/com/example/bigtable/ReadsTest.java
index dc3d56eed6..1af117d638 100644
--- a/samples/snippets/src/test/java/com/example/bigtable/ReadsTest.java
+++ b/samples/snippets/src/test/java/com/example/bigtable/ReadsTest.java
@@ -186,6 +186,37 @@ public void testReadPrefix() {
TIMESTAMP));
}
+ @Test
+ public void testReadRowsReversed() {
+ Reads.readRowsReversed(projectId, instanceId, TABLE_ID);
+ String output = bout.toString();
+
+ assertThat(output)
+ .contains(
+ String.format(
+ "Reading data for phone#5c10102#20190502\n"
+ + "Column Family stats_summary\n"
+ + "\tconnected_cell: \u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0001 @%1$s\n"
+ + "\tconnected_wifi: \u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000 @%1$s\n"
+ + "\tos_build: PQ2A.190406.000 @%1$s"
+ + "Reading data for phone#5c10102#20190501\n"
+ + "Column Family stats_summary\n"
+ + "\tconnected_cell: \u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0001 @%1$s\n"
+ + "\tconnected_wifi: \u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0001 @%1$s\n"
+ + "\tos_build: PQ2A.190401.002 @%1$s\n\n"
+ + "Reading data for phone#4c410523#20190505\n"
+ + "Column Family stats_summary\n"
+ + "\tconnected_cell: \u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000 @%1$s\n"
+ + "\tconnected_wifi: \u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0001 @%1$s\n"
+ + "\tos_build: PQ2A.190406.000 @%1$s\n\n"
+ + "Reading data for phone#4c410523#20190502\n"
+ + "Column Family stats_summary\n"
+ + "\tconnected_cell: \u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0001 @%1$s\n"
+ + "\tconnected_wifi: \u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0001 @%1$s\n"
+ + "\tos_build: PQ2A.190405.004 @%1$s\n\n",
+ TIMESTAMP));
+ }
+
@Test
public void testReadFilter() {
Reads.readFilter(projectId, instanceId, TABLE_ID);