A single-pass input iterator that synchronously fetches rows one at a time.
Dereferencing yields std::pair<error, query_row>: for a data row the error is falsy; if the stream ends with an error, the iterator visits exactly one final element whose error is truthy (and whose row is empty) before comparing equal to end(). A clean end-of-stream yields no such element. This guarantees a for (auto [err, row] : result) loop can observe a terminal error rather than silently stopping.
#include <spdlog/fmt/bundled/chrono.h>
#include <spdlog/fmt/bundled/format.h>
#include <tao/json.hpp>
#include <chrono>
#include <cstddef>
#include <string>
int
main(int argc, const char* argv[])
{
if (argc != 5) {
fmt::print(
"USAGE: ./query_stream_iterator couchbase://127.0.0.1 Administrator password default\n");
return 1;
}
const std::string connection_string{ argv[1] };
const std::string username{ argv[2] };
const std::string password{ argv[3] };
const std::string bucket_name{ argv[4] };
auto [connect_err, cluster] =
.get();
if (connect_err) {
fmt::print("unable to connect to the cluster: {}\n", connect_err);
return 1;
}
auto scope = cluster.
bucket(bucket_name).scope(
"_default");
{
const auto statement = R"(
SELECT META(d).id AS id, d.name AS name, d.price AS price
FROM _default AS d
WHERE d.type = "streaming-example"
ORDER BY d.price DESC
)";
const auto options =
auto [err, result] = scope.query_stream(statement, options).get();
if (err) {
fmt::print("unable to start the streaming query: {}\n", err);
return 1;
}
for (auto [row_err, row] : result) {
if (row_err) {
fmt::print("streaming query failed mid-stream: {}\n", row_err);
return 1;
}
const auto p = row.content_as<couchbase::codec::tao_json_serializer, product>();
fmt::println("{:<24} {:<12} {:>8.2f}", p.id, p.name, p.price);
}
}
{
const auto bulky = R"(SELECT REPEAT("ABCDEFGHIJKLMNOPQRSTUVWXYZ", 200) AS padding
FROM ARRAY_RANGE(0, 15000) AS i)";
const auto buffered_started = std::chrono::steady_clock::now();
auto [buffered_err, buffered] = scope.query(bulky).get();
const auto buffered_elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - buffered_started);
if (buffered_err) {
fmt::print("unable to perform the buffered query: {}\n", buffered_err);
return 1;
}
std::size_t buffered_bytes{ 0 };
for (const auto& row : buffered.rows_as_binary()) {
buffered_bytes += row.size();
}
fmt::println("query(): buffered all {} rows ({:.1f} MiB held at once) in {}",
buffered.rows_as_binary().size(),
static_cast<double>(buffered_bytes) / (1024.0 * 1024.0),
buffered_elapsed);
const auto stream_started = std::chrono::steady_clock::now();
auto [err, result] = scope.query_stream(bulky).get();
if (err) {
fmt::print("unable to start the streaming query: {}\n", err);
return 1;
}
std::size_t seen{ 0 };
for (auto [row_err, row] : result) {
if (row_err) {
fmt::print("streaming query failed mid-stream: {}\n", row_err);
return 1;
}
if (++seen == 3) {
break;
}
}
result.cancel();
const auto stream_elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - stream_started);
fmt::println("query_stream(): read {} rows and cancelled in {} -- the rest was never fetched",
seen,
stream_elapsed);
}
return 0;
}
Part [2] of the error-handling example shows the terminal element being observed rather than silently truncating the result:
#include <spdlog/fmt/bundled/format.h>
#include <chrono>
#include <cstdint>
#include <string>
namespace
{
struct drain_report {
std::uint64_t rows{ 0 };
};
auto
drain(const couchbase::query_stream_result& result) -> drain_report
{
drain_report report{};
while (true) {
auto [row_err, row] = result.next().get();
if (row_err) {
report.error = std::move(row_err);
return report;
}
if (!row) {
return report;
}
++report.rows;
}
}
}
int
main(int argc, const char* argv[])
{
if (argc != 4) {
fmt::print("USAGE: ./query_stream_errors couchbase://127.0.0.1 Administrator password\n");
return 1;
}
const std::string connection_string{ argv[1] };
const std::string username{ argv[2] };
const std::string password{ argv[3] };
auto [connect_err, cluster] =
.get();
if (connect_err) {
fmt::print("unable to connect to the cluster: {}\n", connect_err);
return 1;
}
const auto invalid_statement = R"(SELECT * FROM `nonexistent_keyspace_xyz` LIMIT 1)";
bool saw_error{ false };
{
auto [err, result] = cluster.
query_stream(invalid_statement).get();
if (err) {
saw_error = true;
fmt::println("[1] rejected before streaming began: {}", err);
} else {
auto report = drain(result);
if (report.error) {
saw_error = true;
fmt::println("[1] stream failed after {} row(s): {}", report.rows, report.error);
} else {
fmt::println("[1] stream ended cleanly with {} row(s)", report.rows);
}
}
}
{
auto [err, result] = cluster.
query_stream(invalid_statement).get();
if (err) {
saw_error = true;
fmt::println("[2] rejected before streaming began: {}", err);
} else {
std::uint64_t rows{ 0 };
for (auto [row_err, row] : result) {
if (row_err) {
saw_error = true;
fmt::println("[2] iterator saw the terminal error after {} row(s): {}", rows, row_err);
break;
}
++rows;
}
}
}
{
auto [err, result] = cluster.
query_stream(
"SELECT n FROM ARRAY_RANGE(0, 15000) AS n").get();
if (err) {
fmt::print("unable to start the streaming query: {}\n", err);
return 1;
}
std::uint64_t rows{ 0 };
while (rows < 3) {
auto [row_err, row] = result.next().get();
if (row_err) {
before_cancel = std::move(row_err);
break;
}
if (!row) {
break;
}
++rows;
}
result.cancel();
const auto after_cancel = drain(result);
if (before_cancel) {
fmt::println("[3] the stream failed before it could be cancelled: {}", before_cancel);
fmt::println("[3] stream cancelled after {} row(s); those rows are still valid", rows);
} else {
fmt::println("[3] unexpected terminal after cancel(): \"{}\"",
after_cancel.error.ec().message());
}
auto [again_err, again_row] = result.next().get();
fmt::println("[4] re-reading past the terminal: error=\"{}\", row_present={}",
again_err.ec().message(),
again_row.has_value());
auto [meta_err, meta] = result.meta_data().get();
fmt::println("[5] meta_data() after a torn-down stream: \"{}\"", meta_err.ec().message());
}
{
const auto options = couchbase::query_options{}.readonly(true).timeout(
std::chrono::milliseconds{ 1 });
auto [err, result] =
cluster.
query_stream(
"SELECT n FROM ARRAY_RANGE(0, 15000) AS n", options).get();
std::uint64_t rows{ 0 };
if (!ec) {
const auto report = drain(result);
ec = report.error.ec();
rows = report.rows;
}
fmt::println("[6] read-only statement timed out after {} row(s); retrying it is safe", rows);
fmt::println("[6] statement timed out ambiguously after {} row(s); do not blindly retry",
rows);
} else {
fmt::println("[6] statement did not time out ({} row(s), \"{}\")", rows, ec.message());
}
}
if (!saw_error) {
fmt::print("expected the invalid statement to fail, but it did not\n");
return 1;
}
return 0;
}
This is a minimal single-pass iterator intended for range-based for and manual while (it != result.end()) loops. It compares only against end() (never against another iterator), and provides neither operator-> nor post-increment, so it is deliberately not a LegacyInputIterator and does not work with <algorithm> or std::distance. begin() must be called once.
- Since
- 1.4.0
- Volatile
- Should not be used in production