diff --git a/google-cloud-bigtable/clirr-ignored-differences.xml b/google-cloud-bigtable/clirr-ignored-differences.xml index da5feada67..1ca5867295 100644 --- a/google-cloud-bigtable/clirr-ignored-differences.xml +++ b/google-cloud-bigtable/clirr-ignored-differences.xml @@ -134,4 +134,15 @@ * * + + + 7002 + com/google/cloud/bigtable/data/v2/internal/RowSetUtil + * + + + 7004 + com/google/cloud/bigtable/data/v2/stub/readrows/RowMerger + * + diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/RowMergerUtil.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/RowMergerUtil.java index 9fbc356d53..184dfff623 100644 --- a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/RowMergerUtil.java +++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/data/v2/internal/RowMergerUtil.java @@ -30,7 +30,7 @@ public class RowMergerUtil implements AutoCloseable { public RowMergerUtil() { RowBuilder 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);