kudu-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From t...@apache.org
Subject incubator-kudu git commit: KUDU-1259: new scanner API with an encapsulated Batch object
Date Sat, 23 Jan 2016 00:04:30 GMT
Repository: incubator-kudu
Updated Branches:
  refs/heads/master 05add04e1 -> 75d724451


KUDU-1259: new scanner API with an encapsulated Batch object

This adds a new API for scanner results which encapsulates the result batch,
allowing the caller to access the rows one row at a time, rather than
constructing a vector<KuduRowResult>. This is important in the case that the
result rows are small or empty (for example an empty projection, or scanning a
single int8 column). In those cases, a single batch may return millions or even
tens of millions of rows, in which case the vector<KuduRowResult> was taking up
tens or hundreds of MBs of memory.

The KuduRowResult class itself is renamed to KuduScanBatch::RowPtr, since that
makes it more obvious that the row's lifetime is tied to the batch that it came
from. The old name is preserved via a typedef that will provide for API
compatibility for most users, though it does break the ABI since the
implementation symbols are renamed. Given our beta status, it doesn't seem
necessary to bump the soversion due to this ABI change.

This refactoring ends up transferring the RpcController into the returned
RowBatch object, so it will actually be feasible to use this to avoid
copying strings in Impala -- we can simply attach the KuduScanBatch
to the Impala RowBatch to tie the lifecycle of indirect data to the
lifecycle of the rows in Impala.

I made the appropriate small change in Impala to use the new API and
verified that a SELECT COUNT(*) query which used to take 40+GB of RAM
per server now only uses a few MB. Performance also improved about 10%
for this query, likely due to less allocator pressure and page faults.

The new KuduScanBatch class fits the C++ "iterable sequence" concept,
and thus works with the C++11 range-for loop. Unfortunately it doesn't
seem to work directly with BOOST_FOREACH.

Change-Id: I29fd4fbb8b906ffa591853ab625ac4b089da4bc9
Reviewed-on: http://gerrit.cloudera.org:8080/1562
Tested-by: Internal Jenkins
Reviewed-by: David Ribeiro Alves <david.alves@cloudera.com>


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

Branch: refs/heads/master
Commit: 75d72445161ccdb75fe4a8aa86ad8500335d1149
Parents: 05add04
Author: Todd Lipcon <todd@cloudera.com>
Authored: Wed Nov 25 18:37:29 2015 -0800
Committer: Todd Lipcon <todd@apache.org>
Committed: Sat Jan 23 00:03:30 2016 +0000

----------------------------------------------------------------------
 src/kudu/client/CMakeLists.txt      |   3 +-
 src/kudu/client/client-test.cc      | 154 ++++++++--------
 src/kudu/client/client.cc           |  21 ++-
 src/kudu/client/client.h            |  17 +-
 src/kudu/client/row_result.cc       | 274 ----------------------------
 src/kudu/client/row_result.h        |  93 +---------
 src/kudu/client/scan_batch.cc       | 299 +++++++++++++++++++++++++++++++
 src/kudu/client/scan_batch.h        | 205 +++++++++++++++++++++
 src/kudu/client/scanner-internal.cc | 135 +++++++-------
 src/kudu/client/scanner-internal.h  |  65 +++++--
 src/kudu/rpc/rpc_controller.cc      |  14 ++
 src/kudu/rpc/rpc_controller.h       |   4 +
 src/kudu/tools/ts-cli.cc            |   6 +-
 13 files changed, 762 insertions(+), 528 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-kudu/blob/75d72445/src/kudu/client/CMakeLists.txt
----------------------------------------------------------------------
diff --git a/src/kudu/client/CMakeLists.txt b/src/kudu/client/CMakeLists.txt
index 84cf429..74902a7 100644
--- a/src/kudu/client/CMakeLists.txt
+++ b/src/kudu/client/CMakeLists.txt
@@ -23,7 +23,7 @@ set(CLIENT_SRCS
   error_collector.cc
   error-internal.cc
   meta_cache.cc
-  row_result.cc
+  scan_batch.cc
   scan_predicate.cc
   scanner-internal.cc
   session-internal.cc
@@ -154,6 +154,7 @@ install(FILES
   callbacks.h
   client.h
   row_result.h
+  scan_batch.h
   scan_predicate.h
   schema.h
   shared_ptr.h

http://git-wip-us.apache.org/repos/asf/incubator-kudu/blob/75d72445/src/kudu/client/client-test.cc
----------------------------------------------------------------------
diff --git a/src/kudu/client/client-test.cc b/src/kudu/client/client-test.cc
index 2377716..332b8b3 100644
--- a/src/kudu/client/client-test.cc
+++ b/src/kudu/client/client-test.cc
@@ -255,12 +255,11 @@ class ClientTest : public KuduTest {
       ASSERT_OK(scanner.Open());
 
       ASSERT_TRUE(scanner.HasMoreRows());
-      vector<KuduRowResult> rows;
+      KuduScanBatch batch;
       uint64_t sum = 0;
       while (scanner.HasMoreRows()) {
-        ASSERT_OK(scanner.NextBatch(&rows));
-
-        for (const KuduRowResult& row : rows) {
+        ASSERT_OK(scanner.NextBatch(&batch));
+        for (const KuduScanBatch::RowPtr& row : batch) {
           int32_t value;
           ASSERT_OK(row.GetInt32(0, &value));
           sum += value;
@@ -287,11 +286,10 @@ class ClientTest : public KuduTest {
       ASSERT_OK(scanner.Open());
 
       ASSERT_TRUE(scanner.HasMoreRows());
-      vector<KuduRowResult> rows;
+      KuduScanBatch batch;
       while (scanner.HasMoreRows()) {
-        ASSERT_OK(scanner.NextBatch(&rows));
-
-        for (const KuduRowResult& row : rows) {
+        ASSERT_OK(scanner.NextBatch(&batch));
+        for (const KuduScanBatch::RowPtr& row : batch) {
           Slice s;
           ASSERT_OK(row.GetString(2, &s));
           if (!s.starts_with("hello 2") && !s.starts_with("hello 3")) {
@@ -315,11 +313,10 @@ class ClientTest : public KuduTest {
       ASSERT_OK(scanner.Open());
 
       ASSERT_TRUE(scanner.HasMoreRows());
-      vector<KuduRowResult> rows;
+      KuduScanBatch batch;
       while (scanner.HasMoreRows()) {
-        ASSERT_OK(scanner.NextBatch(&rows));
-
-        for (const KuduRowResult& row : rows) {
+        ASSERT_OK(scanner.NextBatch(&batch));
+        for (const KuduScanBatch::RowPtr& row : batch) {
           int32_t k;
           ASSERT_OK(row.GetInt32(0, &k));
           if (k < 5 || k > 10) {
@@ -357,10 +354,10 @@ class ClientTest : public KuduTest {
     CHECK_OK(scanner.Open());
 
     int count = 0;
-    vector<KuduRowResult> rows;
+    KuduScanBatch batch;
     while (scanner.HasMoreRows()) {
-      CHECK_OK(scanner.NextBatch(&rows));
-      count += rows.size();
+      CHECK_OK(scanner.NextBatch(&batch));
+      count += batch.NumRows();
     }
     return count;
   }
@@ -500,6 +497,8 @@ TEST_F(ClientTest, TestScan) {
   ASSERT_NO_FATAL_FAILURE(InsertTestRows(
       client_table_.get(), FLAGS_test_scan_num_rows));
 
+  ASSERT_EQ(FLAGS_test_scan_num_rows, CountRowsFromClient(client_table_.get()));
+
   // Scan after insert
   DoTestScanWithoutPredicates();
   DoTestScanWithStringPredicate();
@@ -541,16 +540,16 @@ TEST_F(ClientTest, TestScanAtSnapshot) {
 
   KuduScanner scanner(client_table_.get());
   ASSERT_OK(scanner.Open());
-  vector<KuduRowResult> rows;
-  uint64_t sum = 0;
+  uint64_t count = 0;
 
   // Do a "normal", READ_LATEST scan
+  KuduScanBatch batch;
   while (scanner.HasMoreRows()) {
-    ASSERT_OK(scanner.NextBatch(&rows));
-    sum += rows.size();
+    ASSERT_OK(scanner.NextBatch(&batch));
+    count += batch.NumRows();
   }
 
-  ASSERT_EQ(FLAGS_test_scan_num_rows, sum);
+  ASSERT_EQ(FLAGS_test_scan_num_rows, count);
 
   // Now close the scanner and perform a scan at 'ts'
   scanner.Close();
@@ -558,14 +557,13 @@ TEST_F(ClientTest, TestScanAtSnapshot) {
   ASSERT_OK(scanner.SetSnapshotMicros(ts));
   ASSERT_OK(scanner.Open());
 
-  sum = 0;
-
+  count = 0;
   while (scanner.HasMoreRows()) {
-    ASSERT_OK(scanner.NextBatch(&rows));
-    sum += rows.size();
+    ASSERT_OK(scanner.NextBatch(&batch));
+    count += batch.NumRows();
   }
 
-  ASSERT_EQ(half_the_rows, sum);
+  ASSERT_EQ(half_the_rows, count);
 }
 
 // Test scanning at a timestamp in the future compared to the
@@ -716,9 +714,9 @@ TEST_F(ClientTest, TestScanEmptyTable) {
   // the last tablet, HasMoreRows will return true (because it doesn't
   // know whether there's data in subsequent tablets).
   ASSERT_TRUE(scanner.HasMoreRows());
-  vector<KuduRowResult> rows;
-  ASSERT_OK(scanner.NextBatch(&rows));
-  ASSERT_TRUE(rows.empty());
+  KuduScanBatch batch;
+  ASSERT_OK(scanner.NextBatch(&batch));
+  ASSERT_EQ(0, batch.NumRows());
   ASSERT_FALSE(scanner.HasMoreRows());
 }
 
@@ -734,11 +732,11 @@ TEST_F(ClientTest, TestScanEmptyProjection) {
     ASSERT_OK(scanner.Open());
 
     ASSERT_TRUE(scanner.HasMoreRows());
-    vector<KuduRowResult> rows;
+    KuduScanBatch batch;
     uint64_t count = 0;
     while (scanner.HasMoreRows()) {
-      ASSERT_OK(scanner.NextBatch(&rows));
-      count += rows.size();
+      ASSERT_OK(scanner.NextBatch(&batch));
+      count += batch.NumRows();
     }
     ASSERT_EQ(FLAGS_test_scan_num_rows, count);
   }
@@ -777,11 +775,10 @@ TEST_F(ClientTest, TestScanPredicateKeyColNotProjected) {
     ASSERT_OK(scanner.Open());
 
     ASSERT_TRUE(scanner.HasMoreRows());
-    vector<KuduRowResult> rows;
+    KuduScanBatch batch;
     while (scanner.HasMoreRows()) {
-      ASSERT_OK(scanner.NextBatch(&rows));
-
-      for (const KuduRowResult& row : rows) {
+      ASSERT_OK(scanner.NextBatch(&batch));
+      for (const KuduScanBatch::RowPtr& row : batch) {
         int32_t val;
         ASSERT_OK(row.GetInt32(0, &val));
         ASSERT_EQ(curr_key * 2, val);
@@ -815,11 +812,10 @@ TEST_F(ClientTest, TestScanPredicateNonKeyColNotProjected) {
     ASSERT_OK(scanner.Open());
 
     ASSERT_TRUE(scanner.HasMoreRows());
-    vector<KuduRowResult> rows;
+    KuduScanBatch batch;
     while (scanner.HasMoreRows()) {
-      ASSERT_OK(scanner.NextBatch(&rows));
-
-      for (const KuduRowResult& row : rows) {
+      ASSERT_OK(scanner.NextBatch(&batch));
+      for (const KuduScanBatch::RowPtr& row : batch) {
         int32_t val;
         ASSERT_OK(row.GetInt32(0, &val));
         ASSERT_EQ(curr_key / 2, val);
@@ -894,6 +890,14 @@ TEST_F(ClientTest, TestScanCloseProxy) {
 
 namespace internal {
 
+static void ReadBatchToStrings(KuduScanner* scanner, vector<string>* rows) {
+  KuduScanBatch batch;
+  ASSERT_OK(scanner->NextBatch(&batch));
+  for (int i = 0; i < batch.NumRows(); i++) {
+    rows->push_back(batch.Row(i).ToString());
+  }
+}
+
 static void DoScanWithCallback(KuduTable* table,
                                const vector<string>& expected_rows,
                                const boost::function<Status(const string&)>& cb) {
@@ -911,12 +915,8 @@ static void DoScanWithCallback(KuduTable* table,
   {
     LOG(INFO) << "Setting up scanner.";
     ASSERT_TRUE(scanner.HasMoreRows());
-    vector<KuduRowResult> result_rows;
-    ASSERT_OK(scanner.NextBatch(&result_rows));
-    ASSERT_GT(result_rows.size(), 0);
-    for (KuduRowResult& r : result_rows) {
-      rows.push_back(r.ToString());
-    }
+    NO_FATALS(ReadBatchToStrings(&scanner, &rows));
+    ASSERT_GT(rows.size(), 0);
     ASSERT_TRUE(scanner.HasMoreRows());
   }
 
@@ -934,11 +934,7 @@ static void DoScanWithCallback(KuduTable* table,
   ASSERT_TRUE(scanner.HasMoreRows());
   ASSERT_OK(scanner.SetBatchSizeBytes(1024*1024));
   while (scanner.HasMoreRows()) {
-    vector<KuduRowResult> result_rows;
-    ASSERT_OK(scanner.NextBatch(&result_rows));
-    for (KuduRowResult& r : result_rows) {
-      rows.push_back(r.ToString());
-    }
+    NO_FATALS(ReadBatchToStrings(&scanner, &rows));
   }
   scanner.Close();
 
@@ -1206,9 +1202,9 @@ static void AssertScannersDisappear(const tserver::ScannerManager* manager) {
 
 namespace {
 
-int64_t SumResults(const vector<KuduRowResult>& results) {
+int64_t SumResults(const KuduScanBatch& batch) {
   int64_t sum = 0;
-  for (const KuduRowResult row : results) {
+  for (const KuduScanBatch::RowPtr& row : batch) {
     int32_t val;
     CHECK_OK(row.GetInt32(0, &val));
     sum += val;
@@ -1229,15 +1225,15 @@ TEST_F(ClientTest, TestScannerKeepAlive) {
   ASSERT_OK(scanner.SetBatchSizeBytes(100));
   ASSERT_OK(scanner.Open());
 
-  vector<KuduRowResult> results;
+  KuduScanBatch batch;
   int64_t sum = 0;
 
   ASSERT_TRUE(scanner.HasMoreRows());
-  ASSERT_OK(scanner.NextBatch(&results));
+  ASSERT_OK(scanner.NextBatch(&batch));
 
   // We should get only nine rows back (from the first tablet).
-  ASSERT_EQ(results.size(), 9);
-  sum += SumResults(results);
+  ASSERT_EQ(batch.NumRows(), 9);
+  sum += SumResults(batch);
 
   ASSERT_TRUE(scanner.HasMoreRows());
 
@@ -1248,10 +1244,10 @@ TEST_F(ClientTest, TestScannerKeepAlive) {
   // Start scanning the second tablet, but break as soon as we have some data so that
   // we have a live remote scanner on the second tablet.
   while (scanner.HasMoreRows()) {
-    ASSERT_OK(scanner.NextBatch(&results));
-    if (results.size() > 0) break;
+    ASSERT_OK(scanner.NextBatch(&batch));
+    if (batch.NumRows() > 0) break;
   }
-  sum += SumResults(results);
+  sum += SumResults(batch);
   ASSERT_TRUE(scanner.HasMoreRows());
 
   // Now loop while keeping the scanner alive. Each time we loop we sleep 1/2 a scanner
@@ -1265,8 +1261,8 @@ TEST_F(ClientTest, TestScannerKeepAlive) {
   // where we would only actually perform a KeepAlive() rpc after the first request and
   // not on subsequent ones.
   while (scanner.HasMoreRows()) {
-    ASSERT_OK(scanner.NextBatch(&results));
-    if (results.size() > 0) break;
+    ASSERT_OK(scanner.NextBatch(&batch));
+    if (batch.NumRows() > 0) break;
   }
 
   ASSERT_TRUE(scanner.HasMoreRows());
@@ -1274,12 +1270,12 @@ TEST_F(ClientTest, TestScannerKeepAlive) {
     SleepFor(MonoDelta::FromMilliseconds(50));
     ASSERT_OK(scanner.KeepAlive());
   }
-  sum += SumResults(results);
+  sum += SumResults(batch);
 
   // Loop to get the remaining rows.
   while (scanner.HasMoreRows()) {
-    ASSERT_OK(scanner.NextBatch(&results));
-    sum += SumResults(results);
+    ASSERT_OK(scanner.NextBatch(&batch));
+    sum += SumResults(batch);
   }
   ASSERT_FALSE(scanner.HasMoreRows());
   ASSERT_EQ(sum, 499500);
@@ -1370,7 +1366,7 @@ TEST_F(ClientTest, TestScanTimeout) {
     ASSERT_OK(scanner.Open());
     ASSERT_TRUE(scanner.HasMoreRows());
     while (scanner.HasMoreRows()) {
-      vector<KuduRowResult> batch;
+      KuduScanBatch batch;
       ASSERT_OK(scanner.NextBatch(&batch));
     }
   }
@@ -2229,7 +2225,7 @@ namespace {
 void CheckCorrectness(KuduScanner* scanner, int expected[], int nrows) {
   scanner->Open();
   int readrows = 0;
-  vector<KuduRowResult> rows;
+  KuduScanBatch batch;
   if (nrows) {
     ASSERT_TRUE(scanner->HasMoreRows());
   } else {
@@ -2237,8 +2233,8 @@ void CheckCorrectness(KuduScanner* scanner, int expected[], int nrows) {
   }
 
   while (scanner->HasMoreRows()) {
-    ASSERT_OK(scanner->NextBatch(&rows));
-    for (const KuduRowResult& r : rows) {
+    ASSERT_OK(scanner->NextBatch(&batch));
+    for (const KuduScanBatch::RowPtr& r : batch) {
       int32_t key;
       int32_t val;
       Slice strval;
@@ -2413,10 +2409,10 @@ namespace {
     KuduScanner scanner(tbl.get());
 
     scanner.Open();
-    vector<KuduRowResult> rows;
+    KuduScanBatch batch;
     CHECK(scanner.HasMoreRows());
-    CHECK_OK(scanner.NextBatch(&rows));
-    KuduRowResult& row = rows.front();
+    CHECK_OK(scanner.NextBatch(&batch));
+    KuduRowResult row = batch.Row(0);
     int32_t val;
     CHECK_OK(row.GetInt32(1, &val));
     return val;
@@ -2426,11 +2422,11 @@ namespace {
   int CheckRowsEqual(const shared_ptr<KuduTable>& tbl, int32_t expected) {
     KuduScanner scanner(tbl.get());
     scanner.Open();
-    vector<KuduRowResult> rows;
+    KuduScanBatch batch;
     int cnt = 0;
     while (scanner.HasMoreRows()) {
-      CHECK_OK(scanner.NextBatch(&rows));
-      for (const KuduRowResult& row : rows) {
+      CHECK_OK(scanner.NextBatch(&batch));
+      for (const KuduScanBatch::RowPtr& row : batch) {
         // Check that for every key:
         // 1. Column 1 int32_t value == expected
         // 2. Column 2 string value is empty
@@ -2631,10 +2627,10 @@ TEST_F(ClientTest, TestClonePredicates) {
   ASSERT_OK(scanner->Open());
 
   int count = 0;
-  vector<KuduRowResult> rows;
+  KuduScanBatch batch;
   while (scanner->HasMoreRows()) {
-    ASSERT_OK(scanner->NextBatch(&rows));
-    count += rows.size();
+    ASSERT_OK(scanner->NextBatch(&batch));
+    count += batch.NumRows();
   }
 
   ASSERT_EQ(count, 1);
@@ -2645,8 +2641,8 @@ TEST_F(ClientTest, TestClonePredicates) {
 
   count = 0;
   while (scanner->HasMoreRows()) {
-    ASSERT_OK(scanner->NextBatch(&rows));
-    count += rows.size();
+    ASSERT_OK(scanner->NextBatch(&batch));
+    count += batch.NumRows();
   }
 
   ASSERT_EQ(count, 1);

http://git-wip-us.apache.org/repos/asf/incubator-kudu/blob/75d72445/src/kudu/client/client.cc
----------------------------------------------------------------------
diff --git a/src/kudu/client/client.cc b/src/kudu/client/client.cc
index e38a47e..0ae3fca 100644
--- a/src/kudu/client/client.cc
+++ b/src/kudu/client/client.cc
@@ -1132,6 +1132,12 @@ bool KuduScanner::HasMoreRows() const {
 }
 
 Status KuduScanner::NextBatch(vector<KuduRowResult>* rows) {
+  RETURN_NOT_OK(NextBatch(&data_->batch_for_old_api_));
+  data_->batch_for_old_api_.data_->ExtractRows(rows);
+  return Status::OK();
+}
+
+Status KuduScanner::NextBatch(KuduScanBatch* result) {
   // TODO: do some double-buffering here -- when we return this batch
   // we should already have fired off the RPC for the next batch, but
   // need to do some swapping of the response objects around to avoid
@@ -1139,13 +1145,15 @@ Status KuduScanner::NextBatch(vector<KuduRowResult>* rows) {
   CHECK(data_->open_);
   CHECK(data_->proxy_);
 
-  rows->clear();
+  result->data_->Clear();
 
   if (data_->data_in_open_) {
     // We have data from a previous scan.
     VLOG(1) << "Extracting data from scan " << ToString();
     data_->data_in_open_ = false;
-    return data_->ExtractRows(rows);
+    return result->data_->Reset(&data_->controller_,
+                                data_->projection_,
+                                make_gscoped_ptr(data_->last_response_.release_data()));
   } else if (data_->last_response_.has_more_results()) {
     // More data is available in this tablet.
     VLOG(1) << "Continuing scan " << ToString();
@@ -1184,7 +1192,9 @@ Status KuduScanner::NextBatch(vector<KuduRowResult>* rows) {
         data_->last_primary_key_ = data_->last_response_.last_primary_key();
       }
       data_->scan_attempts_ = 0;
-      return data_->ExtractRows(rows);
+      return result->data_->Reset(&data_->controller_,
+                                  data_->projection_,
+                                  make_gscoped_ptr(data_->last_response_.release_data()));
     }
 
     data_->scan_attempts_++;
@@ -1213,7 +1223,6 @@ Status KuduScanner::NextBatch(vector<KuduRowResult>* rows) {
     set<string> blacklist;
     RETURN_NOT_OK(data_->OpenTablet(data_->remote_->partition().partition_key_end(),
                                     deadline, &blacklist));
-
     // No rows written, the next invocation will pick them up.
     return Status::OK();
   } else {
@@ -1238,6 +1247,10 @@ Status KuduScanner::GetCurrentServer(KuduTabletServer** server) {
   return Status::OK();
 }
 
+////////////////////////////////////////////////////////////
+// KuduTabletServer
+////////////////////////////////////////////////////////////
+
 KuduTabletServer::KuduTabletServer()
   : data_(nullptr) {
 }

http://git-wip-us.apache.org/repos/asf/incubator-kudu/blob/75d72445/src/kudu/client/client.h
----------------------------------------------------------------------
diff --git a/src/kudu/client/client.h b/src/kudu/client/client.h
index 2ad8214..1fce8b0 100644
--- a/src/kudu/client/client.h
+++ b/src/kudu/client/client.h
@@ -21,6 +21,8 @@
 #include <string>
 #include <vector>
 
+#include "kudu/client/row_result.h"
+#include "kudu/client/scan_batch.h"
 #include "kudu/client/scan_predicate.h"
 #include "kudu/client/schema.h"
 #include "kudu/client/shared_ptr.h"
@@ -41,14 +43,9 @@ namespace kudu {
 class LinkedListTester;
 class PartitionSchema;
 
-namespace tools {
-class TsAdminClient;
-} // namespace tools
-
 namespace client {
 
 class KuduLoggingCallback;
-class KuduRowResult;
 class KuduSession;
 class KuduStatusCallback;
 class KuduTable;
@@ -973,8 +970,17 @@ class KUDU_EXPORT KuduScanner {
   // Clears 'rows' and populates it with the next batch of rows from the tablet server.
   // A call to NextBatch() invalidates all previously fetched results which might
   // now be pointing to garbage memory.
+  //
+  // DEPRECATED: Use NextBatch(KuduScanBatch*) instead.
   Status NextBatch(std::vector<KuduRowResult>* rows);
 
+  // Fetches the next batch of results for this scanner.
+  //
+  // A single KuduScanBatch instance may be reused. Each subsequent call replaces the data
+  // from the previous call, and invalidates any KuduScanBatch::RowPtr objects previously
+  // obtained from the batch.
+  Status NextBatch(KuduScanBatch* batch);
+
   // Get the KuduTabletServer that is currently handling the scan.
   // More concretely, this is the server that handled the most recent Open or NextBatch
   // RPC made by the server.
@@ -1022,7 +1028,6 @@ class KUDU_EXPORT KuduScanner {
   std::string ToString() const;
  private:
   class KUDU_NO_EXPORT Data;
-  friend class kudu::tools::TsAdminClient;
 
   FRIEND_TEST(ClientTest, TestScanCloseProxy);
   FRIEND_TEST(ClientTest, TestScanFaultTolerance);

http://git-wip-us.apache.org/repos/asf/incubator-kudu/blob/75d72445/src/kudu/client/row_result.cc
----------------------------------------------------------------------
diff --git a/src/kudu/client/row_result.cc b/src/kudu/client/row_result.cc
deleted file mode 100644
index f11c8f6..0000000
--- a/src/kudu/client/row_result.cc
+++ /dev/null
@@ -1,274 +0,0 @@
-// 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.
-
-#include "kudu/client/row_result.h"
-
-#include <string>
-
-#include "kudu/common/schema.h"
-#include "kudu/gutil/strings/substitute.h"
-#include "kudu/util/bitmap.h"
-
-using std::string;
-using strings::Substitute;
-
-namespace kudu {
-namespace client {
-
-namespace {
-
-inline Status FindColumn(const Schema& schema, const Slice& col_name, int* idx) {
-  StringPiece sp(reinterpret_cast<const char*>(col_name.data()), col_name.size());
-  *idx = schema.find_column(sp);
-  if (PREDICT_FALSE(*idx == -1)) {
-    return Status::NotFound("No such column", col_name);
-  }
-  return Status::OK();
-}
-
-// Just enough of a "cell" to support the Schema::DebugCellAppend calls
-// made by KuduRowResult::ToString.
-class RowCell {
- public:
-  RowCell(const KuduRowResult* row, int idx)
-    : row_(row),
-      col_idx_(idx) {
-  }
-
-  bool is_null() const {
-    return row_->IsNull(col_idx_);
-  }
-  const void* ptr() const {
-    return row_->cell(col_idx_);
-  }
-
- private:
-  const KuduRowResult* row_;
-  const int col_idx_;
-};
-
-} // anonymous namespace
-
-bool KuduRowResult::IsNull(int col_idx) const {
-  const ColumnSchema& col = schema_->column(col_idx);
-  if (!col.is_nullable()) {
-    return false;
-  }
-
-  return BitmapTest(row_data_ + schema_->byte_size(), col_idx);
-}
-
-bool KuduRowResult::IsNull(const Slice& col_name) const {
-  int col_idx;
-  CHECK_OK(FindColumn(*schema_, col_name, &col_idx));
-  return IsNull(col_idx);
-}
-
-Status KuduRowResult::GetBool(const Slice& col_name, bool* val) const {
-  return Get<TypeTraits<BOOL> >(col_name, val);
-}
-
-Status KuduRowResult::GetInt8(const Slice& col_name, int8_t* val) const {
-  return Get<TypeTraits<INT8> >(col_name, val);
-}
-
-Status KuduRowResult::GetInt16(const Slice& col_name, int16_t* val) const {
-  return Get<TypeTraits<INT16> >(col_name, val);
-}
-
-Status KuduRowResult::GetInt32(const Slice& col_name, int32_t* val) const {
-  return Get<TypeTraits<INT32> >(col_name, val);
-}
-
-Status KuduRowResult::GetInt64(const Slice& col_name, int64_t* val) const {
-  return Get<TypeTraits<INT64> >(col_name, val);
-}
-
-Status KuduRowResult::GetTimestamp(const Slice& col_name, int64_t* val) const {
-  return Get<TypeTraits<TIMESTAMP> >(col_name, val);
-}
-
-Status KuduRowResult::GetFloat(const Slice& col_name, float* val) const {
-  return Get<TypeTraits<FLOAT> >(col_name, val);
-}
-
-Status KuduRowResult::GetDouble(const Slice& col_name, double* val) const {
-  return Get<TypeTraits<DOUBLE> >(col_name, val);
-}
-
-Status KuduRowResult::GetString(const Slice& col_name, Slice* val) const {
-  return Get<TypeTraits<STRING> >(col_name, val);
-}
-
-Status KuduRowResult::GetBinary(const Slice& col_name, Slice* val) const {
-  return Get<TypeTraits<BINARY> >(col_name, val);
-}
-
-Status KuduRowResult::GetBool(int col_idx, bool* val) const {
-  return Get<TypeTraits<BOOL> >(col_idx, val);
-}
-
-Status KuduRowResult::GetInt8(int col_idx, int8_t* val) const {
-  return Get<TypeTraits<INT8> >(col_idx, val);
-}
-
-Status KuduRowResult::GetInt16(int col_idx, int16_t* val) const {
-  return Get<TypeTraits<INT16> >(col_idx, val);
-}
-
-Status KuduRowResult::GetInt32(int col_idx, int32_t* val) const {
-  return Get<TypeTraits<INT32> >(col_idx, val);
-}
-
-Status KuduRowResult::GetInt64(int col_idx, int64_t* val) const {
-  return Get<TypeTraits<INT64> >(col_idx, val);
-}
-
-Status KuduRowResult::GetTimestamp(int col_idx, int64_t* val) const {
-  return Get<TypeTraits<TIMESTAMP> >(col_idx, val);
-}
-
-Status KuduRowResult::GetFloat(int col_idx, float* val) const {
-  return Get<TypeTraits<FLOAT> >(col_idx, val);
-}
-
-Status KuduRowResult::GetDouble(int col_idx, double* val) const {
-  return Get<TypeTraits<DOUBLE> >(col_idx, val);
-}
-
-Status KuduRowResult::GetString(int col_idx, Slice* val) const {
-  return Get<TypeTraits<STRING> >(col_idx, val);
-}
-
-Status KuduRowResult::GetBinary(int col_idx, Slice* val) const {
-  return Get<TypeTraits<BINARY> >(col_idx, val);
-}
-
-template<typename T>
-Status KuduRowResult::Get(const Slice& col_name, typename T::cpp_type* val) const {
-  int col_idx;
-  RETURN_NOT_OK(FindColumn(*schema_, col_name, &col_idx));
-  return Get<T>(col_idx, val);
-}
-
-template<typename T>
-Status KuduRowResult::Get(int col_idx, typename T::cpp_type* val) const {
-  const ColumnSchema& col = schema_->column(col_idx);
-  if (PREDICT_FALSE(col.type_info()->type() != T::type)) {
-    // TODO: at some point we could allow type coercion here.
-    return Status::InvalidArgument(
-        Substitute("invalid type $0 provided for column '$1' (expected $2)",
-                   T::name(),
-                   col.name(), col.type_info()->name()));
-  }
-
-  if (col.is_nullable() && IsNull(col_idx)) {
-    return Status::NotFound("column is NULL");
-  }
-
-  memcpy(val, row_data_ + schema_->column_offset(col_idx), sizeof(*val));
-  return Status::OK();
-}
-
-const void* KuduRowResult::cell(int col_idx) const {
-  return row_data_ + schema_->column_offset(col_idx);
-}
-
-//------------------------------------------------------------
-// Template instantiations: We instantiate all possible templates to avoid linker issues.
-// see: https://isocpp.org/wiki/faq/templates#separate-template-fn-defn-from-decl
-// TODO We can probably remove this when we move to c++11 and can use "extern template"
-//------------------------------------------------------------
-
-template
-Status KuduRowResult::Get<TypeTraits<BOOL> >(const Slice& col_name, bool* val) const;
-
-template
-Status KuduRowResult::Get<TypeTraits<INT8> >(const Slice& col_name, int8_t* val) const;
-
-template
-Status KuduRowResult::Get<TypeTraits<INT16> >(const Slice& col_name, int16_t* val) const;
-
-template
-Status KuduRowResult::Get<TypeTraits<INT32> >(const Slice& col_name, int32_t* val) const;
-
-template
-Status KuduRowResult::Get<TypeTraits<INT64> >(const Slice& col_name, int64_t* val) const;
-
-template
-Status KuduRowResult::Get<TypeTraits<TIMESTAMP> >(const Slice& col_name, int64_t* val) const;
-
-template
-Status KuduRowResult::Get<TypeTraits<FLOAT> >(const Slice& col_name, float* val) const;
-
-template
-Status KuduRowResult::Get<TypeTraits<DOUBLE> >(const Slice& col_name, double* val) const;
-
-template
-Status KuduRowResult::Get<TypeTraits<STRING> >(const Slice& col_name, Slice* val) const;
-
-template
-Status KuduRowResult::Get<TypeTraits<BINARY> >(const Slice& col_name, Slice* val) const;
-
-template
-Status KuduRowResult::Get<TypeTraits<BOOL> >(int col_idx, bool* val) const;
-
-template
-Status KuduRowResult::Get<TypeTraits<INT8> >(int col_idx, int8_t* val) const;
-
-template
-Status KuduRowResult::Get<TypeTraits<INT16> >(int col_idx, int16_t* val) const;
-
-template
-Status KuduRowResult::Get<TypeTraits<INT32> >(int col_idx, int32_t* val) const;
-
-template
-Status KuduRowResult::Get<TypeTraits<INT64> >(int col_idx, int64_t* val) const;
-
-template
-Status KuduRowResult::Get<TypeTraits<TIMESTAMP> >(int col_idx, int64_t* val) const;
-
-template
-Status KuduRowResult::Get<TypeTraits<FLOAT> >(int col_idx, float* val) const;
-
-template
-Status KuduRowResult::Get<TypeTraits<DOUBLE> >(int col_idx, double* val) const;
-
-template
-Status KuduRowResult::Get<TypeTraits<STRING> >(int col_idx, Slice* val) const;
-
-template
-Status KuduRowResult::Get<TypeTraits<BINARY> >(int col_idx, Slice* val) const;
-
-string KuduRowResult::ToString() const {
-  string ret;
-  ret.append("(");
-  bool first = true;
-  for (int i = 0; i < schema_->num_columns(); i++) {
-    if (!first) {
-      ret.append(", ");
-    }
-    RowCell cell(this, i);
-    schema_->column(i).DebugCellAppend(cell, &ret);
-    first = false;
-  }
-  ret.append(")");
-  return ret;
-}
-
-} // namespace client
-} // namespace kudu

http://git-wip-us.apache.org/repos/asf/incubator-kudu/blob/75d72445/src/kudu/client/row_result.h
----------------------------------------------------------------------
diff --git a/src/kudu/client/row_result.h b/src/kudu/client/row_result.h
index bdf67e2..f1f29ee 100644
--- a/src/kudu/client/row_result.h
+++ b/src/kudu/client/row_result.h
@@ -17,98 +17,17 @@
 #ifndef KUDU_CLIENT_ROW_RESULT_H
 #define KUDU_CLIENT_ROW_RESULT_H
 
-#include <stdint.h>
-#include <string>
-
-#ifdef KUDU_HEADERS_NO_STUBS
-#include "kudu/gutil/port.h"
-#else
-#include "kudu/client/stubs.h"
-#endif
-#include "kudu/util/kudu_export.h"
-#include "kudu/util/slice.h"
-#include "kudu/util/status.h"
+#include "kudu/client/scan_batch.h"
 
 namespace kudu {
-
-class Schema;
-
 namespace client {
 
-// A single row result from a scan.
-//
-// Drawn extensively from ContiguousRow and KuduPartialRow.
-class KUDU_EXPORT KuduRowResult {
- public:
-  bool IsNull(const Slice& col_name) const;
-  bool IsNull(int col_idx) const;
-
-  // These getters return a bad Status if the type does not match,
-  // the value is unset, or the value is NULL. Otherwise they return
-  // the current set value in *val.
-  Status GetBool(const Slice& col_name, bool* val) const WARN_UNUSED_RESULT;
-
-  Status GetInt8(const Slice& col_name, int8_t* val) const WARN_UNUSED_RESULT;
-  Status GetInt16(const Slice& col_name, int16_t* val) const WARN_UNUSED_RESULT;
-  Status GetInt32(const Slice& col_name, int32_t* val) const WARN_UNUSED_RESULT;
-  Status GetInt64(const Slice& col_name, int64_t* val) const WARN_UNUSED_RESULT;
-  Status GetTimestamp(const Slice& col_name, int64_t* micros_since_utc_epoch)
-    const WARN_UNUSED_RESULT;
-
-  Status GetFloat(const Slice& col_name, float* val) const WARN_UNUSED_RESULT;
-  Status GetDouble(const Slice& col_name, double* val) const WARN_UNUSED_RESULT;
-
-  // Same as above getters, but with numeric column indexes.
-  // These are faster since they avoid a hashmap lookup, so should
-  // be preferred in performance-sensitive code.
-  Status GetBool(int col_idx, bool* val) const WARN_UNUSED_RESULT;
-
-  Status GetInt8(int col_idx, int8_t* val) const WARN_UNUSED_RESULT;
-  Status GetInt16(int col_idx, int16_t* val) const WARN_UNUSED_RESULT;
-  Status GetInt32(int col_idx, int32_t* val) const WARN_UNUSED_RESULT;
-  Status GetInt64(int col_idx, int64_t* val) const WARN_UNUSED_RESULT;
-  Status GetTimestamp(int col_idx, int64_t* micros_since_utc_epoch) const WARN_UNUSED_RESULT;
-
-  Status GetFloat(int col_idx, float* val) const WARN_UNUSED_RESULT;
-  Status GetDouble(int col_idx, double* val) const WARN_UNUSED_RESULT;
-
-  // Gets the string/binary value but does not copy the value. Callers should
-  // copy the resulting Slice if necessary.
-  Status GetString(const Slice& col_name, Slice* val) const WARN_UNUSED_RESULT;
-  Status GetString(int col_idx, Slice* val) const WARN_UNUSED_RESULT;
-  Status GetBinary(const Slice& col_name, Slice* val) const WARN_UNUSED_RESULT;
-  Status GetBinary(int col_idx, Slice* val) const WARN_UNUSED_RESULT;
-
-  // Raw cell access. Should be avoided unless absolutely necessary.
-  const void* cell(int col_idx) const;
-
-  std::string ToString() const;
-
- private:
-  friend class KuduScanner;
-  template<typename KeyTypeWrapper> friend struct SliceKeysTestSetup;
-  template<typename KeyTypeWrapper> friend struct IntKeysTestSetup;
-
-  // Only invoked by KuduScanner.
-  //
-  // Why not a constructor? Because we want to create a large number of row
-  // results in one allocation, each with distinct state. Doing that in a
-  // constructor means one allocation per result.
-  void Init(const Schema* schema, const uint8_t* row_data) {
-    schema_ = schema;
-    row_data_ = row_data;
-  }
-
-  template<typename T>
-  Status Get(const Slice& col_name, typename T::cpp_type* val) const;
-
-  template<typename T>
-  Status Get(int col_idx, typename T::cpp_type* val) const;
-
-  const Schema* schema_;
-  const uint8_t* row_data_;
-};
+// DEPRECATED: Kudu 0.7.0 renamed KuduRowResult to KuduScanBatch::RowPtr.
+// The newer name is clearer that the row result's lifetime is tied to the
+// lifetime of a batch.
+typedef KuduScanBatch::RowPtr KuduRowResult;
 
 } // namespace client
 } // namespace kudu
+
 #endif

http://git-wip-us.apache.org/repos/asf/incubator-kudu/blob/75d72445/src/kudu/client/scan_batch.cc
----------------------------------------------------------------------
diff --git a/src/kudu/client/scan_batch.cc b/src/kudu/client/scan_batch.cc
new file mode 100644
index 0000000..f0a1bd7
--- /dev/null
+++ b/src/kudu/client/scan_batch.cc
@@ -0,0 +1,299 @@
+// 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.
+
+#include "kudu/client/row_result.h"
+#include "kudu/client/scan_batch.h"
+#include "kudu/client/scanner-internal.h"
+
+#include <string>
+
+#include "kudu/common/schema.h"
+#include "kudu/gutil/strings/substitute.h"
+#include "kudu/util/bitmap.h"
+
+using std::string;
+using strings::Substitute;
+
+namespace kudu {
+namespace client {
+
+////////////////////////////////////////////////////////////
+// KuduScanBatch
+////////////////////////////////////////////////////////////
+
+KuduScanBatch::KuduScanBatch() : data_(new Data()) {}
+
+KuduScanBatch::~KuduScanBatch() {
+  delete data_;
+}
+
+int KuduScanBatch::NumRows() const {
+  return data_->num_rows();
+}
+
+KuduRowResult KuduScanBatch::Row(int idx) const {
+  return data_->row(idx);
+}
+
+////////////////////////////////////////////////////////////
+// KuduScanBatch::RowPtr
+////////////////////////////////////////////////////////////
+
+namespace {
+
+inline Status FindColumn(const Schema& schema, const Slice& col_name, int* idx) {
+  StringPiece sp(reinterpret_cast<const char*>(col_name.data()), col_name.size());
+  *idx = schema.find_column(sp);
+  if (PREDICT_FALSE(*idx == -1)) {
+    return Status::NotFound("No such column", col_name);
+  }
+  return Status::OK();
+}
+
+// Just enough of a "cell" to support the Schema::DebugCellAppend calls
+// made by KuduScanBatch::RowPtr::ToString.
+class RowCell {
+ public:
+  RowCell(const KuduScanBatch::RowPtr* row, int idx)
+    : row_(row),
+      col_idx_(idx) {
+  }
+
+  bool is_null() const {
+    return row_->IsNull(col_idx_);
+  }
+  const void* ptr() const {
+    return row_->cell(col_idx_);
+  }
+
+ private:
+  const KuduScanBatch::RowPtr* row_;
+  const int col_idx_;
+};
+
+} // anonymous namespace
+
+bool KuduScanBatch::RowPtr::IsNull(int col_idx) const {
+  const ColumnSchema& col = schema_->column(col_idx);
+  if (!col.is_nullable()) {
+    return false;
+  }
+
+  return BitmapTest(row_data_ + schema_->byte_size(), col_idx);
+}
+
+bool KuduScanBatch::RowPtr::IsNull(const Slice& col_name) const {
+  int col_idx;
+  CHECK_OK(FindColumn(*schema_, col_name, &col_idx));
+  return IsNull(col_idx);
+}
+
+Status KuduScanBatch::RowPtr::GetBool(const Slice& col_name, bool* val) const {
+  return Get<TypeTraits<BOOL> >(col_name, val);
+}
+
+Status KuduScanBatch::RowPtr::GetInt8(const Slice& col_name, int8_t* val) const {
+  return Get<TypeTraits<INT8> >(col_name, val);
+}
+
+Status KuduScanBatch::RowPtr::GetInt16(const Slice& col_name, int16_t* val) const {
+  return Get<TypeTraits<INT16> >(col_name, val);
+}
+
+Status KuduScanBatch::RowPtr::GetInt32(const Slice& col_name, int32_t* val) const {
+  return Get<TypeTraits<INT32> >(col_name, val);
+}
+
+Status KuduScanBatch::RowPtr::GetInt64(const Slice& col_name, int64_t* val) const {
+  return Get<TypeTraits<INT64> >(col_name, val);
+}
+
+Status KuduScanBatch::RowPtr::GetTimestamp(const Slice& col_name, int64_t* val) const {
+  return Get<TypeTraits<TIMESTAMP> >(col_name, val);
+}
+
+Status KuduScanBatch::RowPtr::GetFloat(const Slice& col_name, float* val) const {
+  return Get<TypeTraits<FLOAT> >(col_name, val);
+}
+
+Status KuduScanBatch::RowPtr::GetDouble(const Slice& col_name, double* val) const {
+  return Get<TypeTraits<DOUBLE> >(col_name, val);
+}
+
+Status KuduScanBatch::RowPtr::GetString(const Slice& col_name, Slice* val) const {
+  return Get<TypeTraits<STRING> >(col_name, val);
+}
+
+Status KuduScanBatch::RowPtr::GetBinary(const Slice& col_name, Slice* val) const {
+  return Get<TypeTraits<BINARY> >(col_name, val);
+}
+
+Status KuduScanBatch::RowPtr::GetBool(int col_idx, bool* val) const {
+  return Get<TypeTraits<BOOL> >(col_idx, val);
+}
+
+Status KuduScanBatch::RowPtr::GetInt8(int col_idx, int8_t* val) const {
+  return Get<TypeTraits<INT8> >(col_idx, val);
+}
+
+Status KuduScanBatch::RowPtr::GetInt16(int col_idx, int16_t* val) const {
+  return Get<TypeTraits<INT16> >(col_idx, val);
+}
+
+Status KuduScanBatch::RowPtr::GetInt32(int col_idx, int32_t* val) const {
+  return Get<TypeTraits<INT32> >(col_idx, val);
+}
+
+Status KuduScanBatch::RowPtr::GetInt64(int col_idx, int64_t* val) const {
+  return Get<TypeTraits<INT64> >(col_idx, val);
+}
+
+Status KuduScanBatch::RowPtr::GetTimestamp(int col_idx, int64_t* val) const {
+  return Get<TypeTraits<TIMESTAMP> >(col_idx, val);
+}
+
+Status KuduScanBatch::RowPtr::GetFloat(int col_idx, float* val) const {
+  return Get<TypeTraits<FLOAT> >(col_idx, val);
+}
+
+Status KuduScanBatch::RowPtr::GetDouble(int col_idx, double* val) const {
+  return Get<TypeTraits<DOUBLE> >(col_idx, val);
+}
+
+Status KuduScanBatch::RowPtr::GetString(int col_idx, Slice* val) const {
+  return Get<TypeTraits<STRING> >(col_idx, val);
+}
+
+Status KuduScanBatch::RowPtr::GetBinary(int col_idx, Slice* val) const {
+  return Get<TypeTraits<BINARY> >(col_idx, val);
+}
+
+template<typename T>
+Status KuduScanBatch::RowPtr::Get(const Slice& col_name, typename T::cpp_type* val) const {
+  int col_idx;
+  RETURN_NOT_OK(FindColumn(*schema_, col_name, &col_idx));
+  return Get<T>(col_idx, val);
+}
+
+template<typename T>
+Status KuduScanBatch::RowPtr::Get(int col_idx, typename T::cpp_type* val) const {
+  const ColumnSchema& col = schema_->column(col_idx);
+  if (PREDICT_FALSE(col.type_info()->type() != T::type)) {
+    // TODO: at some point we could allow type coercion here.
+    return Status::InvalidArgument(
+        Substitute("invalid type $0 provided for column '$1' (expected $2)",
+                   T::name(),
+                   col.name(), col.type_info()->name()));
+  }
+
+  if (col.is_nullable() && IsNull(col_idx)) {
+    return Status::NotFound("column is NULL");
+  }
+
+  memcpy(val, row_data_ + schema_->column_offset(col_idx), sizeof(*val));
+  return Status::OK();
+}
+
+const void* KuduScanBatch::RowPtr::cell(int col_idx) const {
+  return row_data_ + schema_->column_offset(col_idx);
+}
+
+//------------------------------------------------------------
+// Template instantiations: We instantiate all possible templates to avoid linker issues.
+// see: https://isocpp.org/wiki/faq/templates#separate-template-fn-defn-from-decl
+// TODO We can probably remove this when we move to c++11 and can use "extern template"
+//------------------------------------------------------------
+
+template
+Status KuduScanBatch::RowPtr::Get<TypeTraits<BOOL> >(const Slice& col_name, bool* val) const;
+
+template
+Status KuduScanBatch::RowPtr::Get<TypeTraits<INT8> >(const Slice& col_name, int8_t* val) const;
+
+template
+Status KuduScanBatch::RowPtr::Get<TypeTraits<INT16> >(const Slice& col_name, int16_t* val) const;
+
+template
+Status KuduScanBatch::RowPtr::Get<TypeTraits<INT32> >(const Slice& col_name, int32_t* val) const;
+
+template
+Status KuduScanBatch::RowPtr::Get<TypeTraits<INT64> >(const Slice& col_name, int64_t* val) const;
+
+template
+Status KuduScanBatch::RowPtr::Get<TypeTraits<TIMESTAMP> >(
+    const Slice& col_name, int64_t* val) const;
+
+template
+Status KuduScanBatch::RowPtr::Get<TypeTraits<FLOAT> >(const Slice& col_name, float* val) const;
+
+template
+Status KuduScanBatch::RowPtr::Get<TypeTraits<DOUBLE> >(const Slice& col_name, double* val) const;
+
+template
+Status KuduScanBatch::RowPtr::Get<TypeTraits<STRING> >(const Slice& col_name, Slice* val) const;
+
+template
+Status KuduScanBatch::RowPtr::Get<TypeTraits<BINARY> >(const Slice& col_name, Slice* val) const;
+
+template
+Status KuduScanBatch::RowPtr::Get<TypeTraits<BOOL> >(int col_idx, bool* val) const;
+
+template
+Status KuduScanBatch::RowPtr::Get<TypeTraits<INT8> >(int col_idx, int8_t* val) const;
+
+template
+Status KuduScanBatch::RowPtr::Get<TypeTraits<INT16> >(int col_idx, int16_t* val) const;
+
+template
+Status KuduScanBatch::RowPtr::Get<TypeTraits<INT32> >(int col_idx, int32_t* val) const;
+
+template
+Status KuduScanBatch::RowPtr::Get<TypeTraits<INT64> >(int col_idx, int64_t* val) const;
+
+template
+Status KuduScanBatch::RowPtr::Get<TypeTraits<TIMESTAMP> >(int col_idx, int64_t* val) const;
+
+template
+Status KuduScanBatch::RowPtr::Get<TypeTraits<FLOAT> >(int col_idx, float* val) const;
+
+template
+Status KuduScanBatch::RowPtr::Get<TypeTraits<DOUBLE> >(int col_idx, double* val) const;
+
+template
+Status KuduScanBatch::RowPtr::Get<TypeTraits<STRING> >(int col_idx, Slice* val) const;
+
+template
+Status KuduScanBatch::RowPtr::Get<TypeTraits<BINARY> >(int col_idx, Slice* val) const;
+
+string KuduScanBatch::RowPtr::ToString() const {
+  string ret;
+  ret.append("(");
+  bool first = true;
+  for (int i = 0; i < schema_->num_columns(); i++) {
+    if (!first) {
+      ret.append(", ");
+    }
+    RowCell cell(this, i);
+    schema_->column(i).DebugCellAppend(cell, &ret);
+    first = false;
+  }
+  ret.append(")");
+  return ret;
+}
+
+} // namespace client
+} // namespace kudu

http://git-wip-us.apache.org/repos/asf/incubator-kudu/blob/75d72445/src/kudu/client/scan_batch.h
----------------------------------------------------------------------
diff --git a/src/kudu/client/scan_batch.h b/src/kudu/client/scan_batch.h
new file mode 100644
index 0000000..c08130a
--- /dev/null
+++ b/src/kudu/client/scan_batch.h
@@ -0,0 +1,205 @@
+// 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.
+#ifndef KUDU_CLIENT_SCAN_BATCH_H
+#define KUDU_CLIENT_SCAN_BATCH_H
+
+#include <string>
+
+#include "kudu/util/kudu_export.h"
+#include "kudu/util/slice.h"
+
+namespace kudu {
+class Schema;
+
+namespace tools {
+class TsAdminClient;
+} // namespace tools
+
+namespace client {
+
+// A batch of zero or more rows returned from a KuduScanner.
+//
+// With C++11, you can iterate over the rows in the batch using a
+// range-foreach loop:
+//
+//   for (KuduScanBatch::RowPtr row : batch) {
+//     ... row.GetInt(1, ...)
+//     ...
+//   }
+//
+// In C++03, you'll need to use a regular for loop:
+//
+//   for (int i = 0, num_rows = batch.NumRows();
+//        i < num_rows;
+//        i++) {
+//     KuduScanBatch::RowPtr row = batch.Row(i);
+//     ...
+//   }
+//
+// Note that, in the above example, NumRows() is only called once at the
+// beginning of the loop to avoid extra calls to the non-inlined method.
+class KUDU_EXPORT KuduScanBatch {
+ public:
+  class RowPtr;
+  class const_iterator;
+  typedef RowPtr value_type;
+
+  KuduScanBatch();
+  ~KuduScanBatch();
+
+  // Return the number of rows in this batch.
+  int NumRows() const;
+
+  // Return a reference to one of the rows in this batch.
+  // The returned object is only valid for as long as this KuduScanBatch.
+  KuduScanBatch::RowPtr Row(int idx) const;
+
+  const_iterator begin() const;
+  const_iterator end() const;
+
+ private:
+  class KUDU_NO_EXPORT Data;
+  friend class KuduScanner;
+  friend class kudu::tools::TsAdminClient;
+
+  Data* data_;
+  DISALLOW_COPY_AND_ASSIGN(KuduScanBatch);
+};
+
+// A single row result from a scan. Note that this object acts as a pointer into
+// a KuduScanBatch, and therefore is valid only as long as the batch it was constructed
+// from.
+class KUDU_EXPORT KuduScanBatch::RowPtr {
+ public:
+  // Construct an invalid RowPtr. Before use, you must assign
+  // a properly-initialized value.
+  RowPtr() : schema_(NULL), row_data_(NULL) {}
+
+  bool IsNull(const Slice& col_name) const;
+  bool IsNull(int col_idx) const;
+
+  // These getters return a bad Status if the type does not match,
+  // the value is unset, or the value is NULL. Otherwise they return
+  // the current set value in *val.
+  Status GetBool(const Slice& col_name, bool* val) const WARN_UNUSED_RESULT;
+
+  Status GetInt8(const Slice& col_name, int8_t* val) const WARN_UNUSED_RESULT;
+  Status GetInt16(const Slice& col_name, int16_t* val) const WARN_UNUSED_RESULT;
+  Status GetInt32(const Slice& col_name, int32_t* val) const WARN_UNUSED_RESULT;
+  Status GetInt64(const Slice& col_name, int64_t* val) const WARN_UNUSED_RESULT;
+  Status GetTimestamp(const Slice& col_name, int64_t* micros_since_utc_epoch)
+    const WARN_UNUSED_RESULT;
+
+  Status GetFloat(const Slice& col_name, float* val) const WARN_UNUSED_RESULT;
+  Status GetDouble(const Slice& col_name, double* val) const WARN_UNUSED_RESULT;
+
+  // Same as above getters, but with numeric column indexes.
+  // These are faster since they avoid a hashmap lookup, so should
+  // be preferred in performance-sensitive code.
+  Status GetBool(int col_idx, bool* val) const WARN_UNUSED_RESULT;
+
+  Status GetInt8(int col_idx, int8_t* val) const WARN_UNUSED_RESULT;
+  Status GetInt16(int col_idx, int16_t* val) const WARN_UNUSED_RESULT;
+  Status GetInt32(int col_idx, int32_t* val) const WARN_UNUSED_RESULT;
+  Status GetInt64(int col_idx, int64_t* val) const WARN_UNUSED_RESULT;
+  Status GetTimestamp(int col_idx, int64_t* micros_since_utc_epoch) const WARN_UNUSED_RESULT;
+
+  Status GetFloat(int col_idx, float* val) const WARN_UNUSED_RESULT;
+  Status GetDouble(int col_idx, double* val) const WARN_UNUSED_RESULT;
+
+  // Gets the string/binary value but does not copy the value. Callers should
+  // copy the resulting Slice if necessary.
+  Status GetString(const Slice& col_name, Slice* val) const WARN_UNUSED_RESULT;
+  Status GetString(int col_idx, Slice* val) const WARN_UNUSED_RESULT;
+  Status GetBinary(const Slice& col_name, Slice* val) const WARN_UNUSED_RESULT;
+  Status GetBinary(int col_idx, Slice* val) const WARN_UNUSED_RESULT;
+
+  // Raw cell access. Should be avoided unless absolutely necessary.
+  const void* cell(int col_idx) const;
+
+  std::string ToString() const;
+
+ private:
+  friend class KuduScanBatch;
+  template<typename KeyTypeWrapper> friend struct SliceKeysTestSetup;
+  template<typename KeyTypeWrapper> friend struct IntKeysTestSetup;
+
+  // Only invoked by KuduScanner.
+  RowPtr(const Schema* schema, const uint8_t* row_data)
+      : schema_(schema),
+      row_data_(row_data) {
+  }
+
+  template<typename T>
+  Status Get(const Slice& col_name, typename T::cpp_type* val) const;
+
+  template<typename T>
+  Status Get(int col_idx, typename T::cpp_type* val) const;
+
+  const Schema* schema_;
+  const uint8_t* row_data_;
+};
+
+// C++ forward iterator over the rows in a KuduScanBatch.
+//
+// This iterator yields KuduScanBatch::RowPtr objects which point inside the row batch
+// itself. Thus, the iterator and any objects obtained from it are invalidated if the
+// KuduScanBatch is destroyed or used for a new NextBatch() call.
+class KUDU_EXPORT KuduScanBatch::const_iterator
+    : public std::iterator<std::forward_iterator_tag, KuduScanBatch::RowPtr> {
+ public:
+  ~const_iterator() {}
+
+  KuduScanBatch::RowPtr operator*() const {
+    return batch_->Row(idx_);
+  }
+
+  void operator++() {
+    idx_++;
+  }
+
+  bool operator==(const const_iterator& other) {
+    return idx_ == other.idx_;
+  }
+  bool operator!=(const const_iterator& other) {
+    return idx_ != other.idx_;
+  }
+
+ private:
+  friend class KuduScanBatch;
+  const_iterator(const KuduScanBatch* b, int idx)
+      : batch_(b),
+        idx_(idx) {
+  }
+
+  const KuduScanBatch* batch_;
+  int idx_;
+};
+
+
+inline KuduScanBatch::const_iterator KuduScanBatch::begin() const {
+  return const_iterator(this, 0);
+}
+
+inline KuduScanBatch::const_iterator KuduScanBatch::end() const {
+  return const_iterator(this, NumRows());
+}
+
+} // namespace client
+} // namespace kudu
+
+#endif

http://git-wip-us.apache.org/repos/asf/incubator-kudu/blob/75d72445/src/kudu/client/scanner-internal.cc
----------------------------------------------------------------------
diff --git a/src/kudu/client/scanner-internal.cc b/src/kudu/client/scanner-internal.cc
index 3fee9e9..187c943 100644
--- a/src/kudu/client/scanner-internal.cc
+++ b/src/kudu/client/scanner-internal.cc
@@ -369,67 +369,6 @@ Status KuduScanner::Data::KeepAlive() {
   return Status::OK();
 }
 
-Status KuduScanner::Data::ExtractRows(vector<KuduRowResult>* rows) {
-  return ExtractRows(controller_, projection_, &last_response_, rows);
-}
-
-Status KuduScanner::Data::ExtractRows(const RpcController& controller,
-                                      const Schema* projection,
-                                      ScanResponsePB* resp,
-                                      vector<KuduRowResult>* rows) {
-  // First, rewrite the relative addresses into absolute ones.
-  RowwiseRowBlockPB* rowblock_pb = resp->mutable_data();
-  Slice direct, indirect;
-
-  if (PREDICT_FALSE(!rowblock_pb->has_rows_sidecar())) {
-    return Status::Corruption("Server sent invalid response: no row data");
-  } else {
-    Status s = controller.GetSidecar(rowblock_pb->rows_sidecar(), &direct);
-    if (!s.ok()) {
-      return Status::Corruption("Server sent invalid response: row data "
-                                "sidecar index corrupt", s.ToString());
-    }
-  }
-
-  if (rowblock_pb->has_indirect_data_sidecar()) {
-    Status s = controller.GetSidecar(rowblock_pb->indirect_data_sidecar(),
-                                      &indirect);
-    if (!s.ok()) {
-      return Status::Corruption("Server sent invalid response: indirect data "
-                                "sidecar index corrupt", s.ToString());
-    }
-  }
-
-  RETURN_NOT_OK(RewriteRowBlockPointers(*projection, *rowblock_pb, indirect, &direct));
-
-  int n_rows = rowblock_pb->num_rows();
-  if (PREDICT_FALSE(n_rows == 0)) {
-    // Early-out here to avoid a UBSAN failure.
-    VLOG(1) << "Extracted 0 rows";
-    return Status::OK();
-  }
-
-  // Next, allocate a block of KuduRowResults in 'rows'.
-  size_t before = rows->size();
-  rows->resize(before + n_rows);
-
-  // Lastly, initialize each KuduRowResult with data from the response.
-  //
-  // Doing this resize and array indexing turns out to be noticeably faster
-  // than using reserve and push_back.
-  int projected_row_size = CalculateProjectedRowSize(*projection);
-  const uint8_t* src = direct.data();
-  KuduRowResult* dst = &(*rows)[before];
-  while (n_rows > 0) {
-    dst->Init(projection, src);
-    dst++;
-    src += projected_row_size;
-    n_rows--;
-  }
-  VLOG(1) << "Extracted " << rows->size() - before << " rows";
-  return Status::OK();
-}
-
 bool KuduScanner::Data::MoreTablets() const {
   CHECK(open_);
   // TODO(KUDU-565): add a test which has a scan end on a tablet boundary
@@ -476,10 +415,82 @@ void KuduScanner::Data::PrepareRequest(RequestType state) {
   }
 }
 
-size_t KuduScanner::Data::CalculateProjectedRowSize(const Schema& proj) {
+
+////////////////////////////////////////////////////////////
+// KuduScanBatch
+////////////////////////////////////////////////////////////
+
+KuduScanBatch::Data::Data() : projection_(NULL) {}
+
+KuduScanBatch::Data::~Data() {}
+
+size_t KuduScanBatch::Data::CalculateProjectedRowSize(const Schema& proj) {
   return proj.byte_size() +
         (proj.has_nullables() ? BitmapSize(proj.num_columns()) : 0);
 }
 
+Status KuduScanBatch::Data::Reset(RpcController* controller,
+                                  const Schema* projection,
+                                  gscoped_ptr<RowwiseRowBlockPB> data) {
+  CHECK(controller->finished());
+  controller_.Swap(controller);
+  projection_ = projection;
+  resp_data_.Swap(data.get());
+
+  // First, rewrite the relative addresses into absolute ones.
+  if (PREDICT_FALSE(!resp_data_.has_rows_sidecar())) {
+    return Status::Corruption("Server sent invalid response: no row data");
+  } else {
+    Status s = controller_.GetSidecar(resp_data_.rows_sidecar(), &direct_data_);
+    if (!s.ok()) {
+      return Status::Corruption("Server sent invalid response: row data "
+                                "sidecar index corrupt", s.ToString());
+    }
+  }
+
+  if (resp_data_.has_indirect_data_sidecar()) {
+    Status s = controller_.GetSidecar(resp_data_.indirect_data_sidecar(),
+                                      &indirect_data_);
+    if (!s.ok()) {
+      return Status::Corruption("Server sent invalid response: indirect data "
+                                "sidecar index corrupt", s.ToString());
+    }
+  }
+
+  RETURN_NOT_OK(RewriteRowBlockPointers(*projection_, resp_data_, indirect_data_, &direct_data_));
+  projected_row_size_ = CalculateProjectedRowSize(*projection_);
+  return Status::OK();
+}
+
+void KuduScanBatch::Data::ExtractRows(vector<KuduScanBatch::RowPtr>* rows) {
+  int n_rows = resp_data_.num_rows();
+  rows->resize(n_rows);
+
+  if (PREDICT_FALSE(n_rows == 0)) {
+    // Early-out here to avoid a UBSAN failure.
+    VLOG(1) << "Extracted 0 rows";
+    return;
+  }
+
+  // Initialize each RowPtr with data from the response.
+  //
+  // Doing this resize and array indexing turns out to be noticeably faster
+  // than using reserve and push_back.
+  const uint8_t* src = direct_data_.data();
+  KuduScanBatch::RowPtr* dst = &(*rows)[0];
+  while (n_rows > 0) {
+    *dst = KuduScanBatch::RowPtr(projection_, src);
+    dst++;
+    src += projected_row_size_;
+    n_rows--;
+  }
+  VLOG(1) << "Extracted " << rows->size() << " rows";
+}
+
+void KuduScanBatch::Data::Clear() {
+  resp_data_.Clear();
+  controller_.Reset();
+}
+
 } // namespace client
 } // namespace kudu

http://git-wip-us.apache.org/repos/asf/incubator-kudu/blob/75d72445/src/kudu/client/scanner-internal.h
----------------------------------------------------------------------
diff --git a/src/kudu/client/scanner-internal.h b/src/kudu/client/scanner-internal.h
index 3ac321f..b7a1ddc 100644
--- a/src/kudu/client/scanner-internal.h
+++ b/src/kudu/client/scanner-internal.h
@@ -23,6 +23,7 @@
 
 #include "kudu/gutil/macros.h"
 #include "kudu/client/client.h"
+#include "kudu/client/row_result.h"
 #include "kudu/common/scan_spec.h"
 #include "kudu/common/predicate_encoder.h"
 #include "kudu/tserver/tserver_service.proxy.h"
@@ -65,16 +66,6 @@ class KuduScanner::Data {
                     const MonoTime& deadline,
                     std::set<std::string>* blacklist);
 
-  // Extracts data from the last scan response and adds them to 'rows'.
-  Status ExtractRows(std::vector<KuduRowResult>* rows);
-
-  // Static implementation of ExtractRows. This is used by some external
-  // tools.
-  static Status ExtractRows(const rpc::RpcController& controller,
-                            const Schema* projection,
-                            tserver::ScanResponsePB* resp,
-                            std::vector<KuduRowResult>* rows);
-
   Status KeepAlive();
 
   // Returns whether there exist more tablets we should scan.
@@ -99,9 +90,6 @@ class KuduScanner::Data {
   // Modifies fields in 'next_req_' in preparation for a new request.
   void PrepareRequest(RequestType state);
 
-  // Returns the size of a row for the given projection 'proj'.
-  static size_t CalculateProjectedRowSize(const Schema& proj);
-
   bool open_;
   bool data_in_open_;
   bool has_batch_size_bytes_;
@@ -154,9 +142,60 @@ class KuduScanner::Data {
   // Number of attempts since the last successful scan.
   int scan_attempts_;
 
+  // The deprecated "NextBatch(vector<KuduRowResult>*) API requires some local
+  // storage for the actual row data. If that API is used, this member keeps the
+  // actual storage for the batch that is returned.
+  KuduScanBatch batch_for_old_api_;
+
   DISALLOW_COPY_AND_ASSIGN(Data);
 };
 
+class KuduScanBatch::Data {
+ public:
+  Data();
+  ~Data();
+
+  Status Reset(rpc::RpcController* controller,
+               const Schema* projection,
+               gscoped_ptr<RowwiseRowBlockPB> resp_data);
+
+  int num_rows() const {
+    return resp_data_.num_rows();
+  }
+
+  KuduRowResult row(int idx) {
+    DCHECK_GE(idx, 0);
+    DCHECK_LT(idx, num_rows());
+    int offset = idx * projected_row_size_;
+    return KuduRowResult(projection_, &direct_data_[offset]);
+  }
+
+  void ExtractRows(vector<KuduScanBatch::RowPtr>* rows);
+
+  void Clear();
+
+  // Returns the size of a row for the given projection 'proj'.
+  static size_t CalculateProjectedRowSize(const Schema& proj);
+
+  // The RPC controller for the RPC which returned this batch.
+  // Holding on to the controller ensures we hold on to the indirect data
+  // which contains the rows.
+  rpc::RpcController controller_;
+
+  // The PB which contains the "direct data" slice.
+  RowwiseRowBlockPB resp_data_;
+
+  // Slices into the direct and indirect row data, whose lifetime is ensured
+  // by the members above.
+  Slice direct_data_, indirect_data_;
+
+  // The projection being scanned.
+  const Schema* projection_;
+
+  // The number of bytes of direct data for each row.
+  size_t projected_row_size_;
+};
+
 } // namespace client
 } // namespace kudu
 

http://git-wip-us.apache.org/repos/asf/incubator-kudu/blob/75d72445/src/kudu/rpc/rpc_controller.cc
----------------------------------------------------------------------
diff --git a/src/kudu/rpc/rpc_controller.cc b/src/kudu/rpc/rpc_controller.cc
index 672ac12..53f76d9 100644
--- a/src/kudu/rpc/rpc_controller.cc
+++ b/src/kudu/rpc/rpc_controller.cc
@@ -15,6 +15,7 @@
 // specific language governing permissions and limitations
 // under the License.
 
+#include <algorithm>
 #include <glog/logging.h>
 
 #include "kudu/rpc/rpc_controller.h"
@@ -30,6 +31,19 @@ RpcController::~RpcController() {
   DVLOG(4) << "RpcController " << this << " destroyed";
 }
 
+void RpcController::Swap(RpcController* other) {
+  // Cannot swap RPC controllers while they are in-flight.
+  if (call_) {
+    CHECK(finished());
+  }
+  if (other->call_) {
+    CHECK(other->finished());
+  }
+
+  std::swap(timeout_, other->timeout_);
+  std::swap(call_, other->call_);
+}
+
 void RpcController::Reset() {
   lock_guard<simple_spinlock> l(&lock_);
   if (call_) {

http://git-wip-us.apache.org/repos/asf/incubator-kudu/blob/75d72445/src/kudu/rpc/rpc_controller.h
----------------------------------------------------------------------
diff --git a/src/kudu/rpc/rpc_controller.h b/src/kudu/rpc/rpc_controller.h
index 99b4ed4..bea703b 100644
--- a/src/kudu/rpc/rpc_controller.h
+++ b/src/kudu/rpc/rpc_controller.h
@@ -46,6 +46,10 @@ class RpcController {
   RpcController();
   ~RpcController();
 
+  // Swap the state of the controller (including ownership of sidecars, buffers,
+  // etc) with another one.
+  void Swap(RpcController* other);
+
   // Reset this controller so it may be used with another call.
   void Reset();
 

http://git-wip-us.apache.org/repos/asf/incubator-kudu/blob/75d72445/src/kudu/tools/ts-cli.cc
----------------------------------------------------------------------
diff --git a/src/kudu/tools/ts-cli.cc b/src/kudu/tools/ts-cli.cc
index 663e720..d0f2eb8 100644
--- a/src/kudu/tools/ts-cli.cc
+++ b/src/kudu/tools/ts-cli.cc
@@ -44,13 +44,13 @@
 #include "kudu/rpc/rpc_controller.h"
 
 using kudu::client::KuduRowResult;
-using kudu::client::KuduScanner;
 using kudu::HostPort;
 using kudu::rpc::Messenger;
 using kudu::rpc::MessengerBuilder;
 using kudu::rpc::RpcController;
 using kudu::server::ServerStatusPB;
 using kudu::Sockaddr;
+using kudu::client::KuduScanBatch;
 using kudu::tablet::TabletStatusPB;
 using kudu::tserver::DeleteTabletRequestPB;
 using kudu::tserver::DeleteTabletResponsePB;
@@ -273,7 +273,9 @@ Status TsAdminClient::DumpTablet(const std::string& tablet_id) {
     }
 
     rows.clear();
-    RETURN_NOT_OK(KuduScanner::Data::ExtractRows(rpc, &schema, &resp, &rows));
+    KuduScanBatch::Data results;
+    RETURN_NOT_OK(results.Reset(&rpc, &schema, make_gscoped_ptr(resp.release_data())));
+    results.ExtractRows(&rows);
     for (const KuduRowResult& r : rows) {
       std::cout << r.ToString() << std::endl;
     }


Mime
View raw message