diff --git a/google/cloud/bigtable/value.cc b/google/cloud/bigtable/value.cc index 0fea5cada96c7..4190d293cb036 100644 --- a/google/cloud/bigtable/value.cc +++ b/google/cloud/bigtable/value.cc @@ -50,6 +50,24 @@ std::string AsString(T&& s) { std::move(s)); // NOLINT(bugprone-move-forwarding-reference) } +// Forward declarations for mutually recursive functions. +bool Equal(google::bigtable::v2::Type const& pt1, // NOLINT(misc-no-recursion) + google::bigtable::v2::Value const& pv1, + google::bigtable::v2::Type const& pt2, + google::bigtable::v2::Value const& pv2); + +bool ArrayEqual( // NOLINT(misc-no-recursion) + google::bigtable::v2::Type const& pt1, + google::bigtable::v2::Value const& pv1, + google::bigtable::v2::Type const& pt2, + google::bigtable::v2::Value const& pv2); + +bool StructEqual( // NOLINT(misc-no-recursion) + google::bigtable::v2::Type const& pt1, + google::bigtable::v2::Value const& pv1, + google::bigtable::v2::Type const& pt2, + google::bigtable::v2::Value const& pv2); + // Compares two sets of Type and Value protos for equality. This method calls // itself recursively to compare subtypes and subvalues. bool Equal(google::bigtable::v2::Type const& pt1, // NOLINT(misc-no-recursion) @@ -83,24 +101,61 @@ bool Equal(google::bigtable::v2::Type const& pt1, // NOLINT(misc-no-recursion) pv1.date_value().year() == pv2.date_value().year(); } if (pt1.has_array_type()) { - auto const& vec1 = pv1.array_value().values(); - auto const& vec2 = pv2.array_value().values(); - if (vec1.size() != vec2.size()) { + return ArrayEqual(pt1, pv1, pt2, pv2); + } + if (pt1.has_struct_type()) { + return StructEqual(pt1, pv1, pt2, pv2); + } + return false; +} + +// Compares two sets of Type and Value protos that represent an ARRAY for +// equality. +bool ArrayEqual( // NOLINT(misc-no-recursion) + google::bigtable::v2::Type const& pt1, + google::bigtable::v2::Value const& pv1, + google::bigtable::v2::Type const& pt2, + google::bigtable::v2::Value const& pv2) { + auto const& vec1 = pv1.array_value().values(); + auto const& vec2 = pv2.array_value().values(); + if (vec1.size() != vec2.size()) { + return false; + } + auto const& el_type1 = pt1.array_type().element_type(); + auto const& el_type2 = pt2.array_type().element_type(); + if (el_type1.kind_case() != el_type2.kind_case()) { + return false; + } + for (int i = 0; i < vec1.size(); ++i) { + if (!Equal(el_type1, vec1.Get(i), el_type2, vec2.Get(i))) { return false; } - auto const& el_type1 = pt1.array_type().element_type(); - auto const& el_type2 = pt2.array_type().element_type(); - if (el_type1.kind_case() != el_type2.kind_case()) { + } + return true; +} + +// Compares two sets of Type and Value protos that represent a STRUCT for +// equality. +bool StructEqual( // NOLINT(misc-no-recursion) + google::bigtable::v2::Type const& pt1, + google::bigtable::v2::Value const& pv1, + google::bigtable::v2::Type const& pt2, + google::bigtable::v2::Value const& pv2) { + auto const& fields1 = pt1.struct_type().fields(); + auto const& fields2 = pt2.struct_type().fields(); + if (fields1.size() != fields2.size()) return false; + auto const& v1 = pv1.array_value().values(); + auto const& v2 = pv2.array_value().values(); + if (fields1.size() != v1.size() || v1.size() != v2.size()) return false; + for (int i = 0; i < fields1.size(); ++i) { + auto const& f1 = fields1.Get(i); + auto const& f2 = fields2.Get(i); + if (f1.field_name() != f2.field_name()) return false; + if (!Equal(f1.type(), v1.Get(i), f2.type(), v2.Get(i))) { return false; } - for (int i = 0; i < vec1.size(); ++i) { - if (!Equal(el_type1, vec1.Get(i), el_type2, vec2.Get(i))) { - return false; - } - } - return true; } - return false; + return true; } // From the proto description, `NULL` values are represented by having a kind @@ -133,16 +188,17 @@ std::ostream& StreamHelper(std::ostream& os, // NOLINT(misc-no-recursion) return os << "NULL"; } - if (v.kind_case() == google::bigtable::v2::Value::kBoolValue) { + if (t.kind_case() == google::bigtable::v2::Type::kBoolType) { return os << v.bool_value(); } - if (v.kind_case() == google::bigtable::v2::Value::kIntValue) { + if (t.kind_case() == google::bigtable::v2::Type::kInt64Type) { return os << v.int_value(); } - if (v.kind_case() == google::bigtable::v2::Value::kFloatValue) { + if (t.kind_case() == google::bigtable::v2::Type::kFloat32Type || + t.kind_case() == google::bigtable::v2::Type::kFloat64Type) { return os << v.float_value(); } - if (v.kind_case() == google::bigtable::v2::Value::kStringValue) { + if (t.kind_case() == google::bigtable::v2::Type::kStringType) { switch (mode) { case StreamMode::kScalar: return os << v.string_value(); @@ -153,22 +209,22 @@ std::ostream& StreamHelper(std::ostream& os, // NOLINT(misc-no-recursion) } return os; // Unreachable, but quiets warning. } - if (v.kind_case() == google::bigtable::v2::Value::kBytesValue) { + if (t.kind_case() == google::bigtable::v2::Type::kBytesType) { return os << Bytes(AsString(v.bytes_value())); } - if (v.kind_case() == google::bigtable::v2::Value::kTimestampValue) { + if (t.kind_case() == google::bigtable::v2::Type::kTimestampType) { auto ts = MakeTimestamp(v.timestamp_value()); if (!ts) { internal::ThrowStatus(ts.status()); } return os << ts.value(); } - if (v.kind_case() == google::bigtable::v2::Value::kDateValue) { + if (t.kind_case() == google::bigtable::v2::Type::kDateType) { auto date = bigtable_internal::FromProto(t, v).get().value(); return os << date; } - if (v.kind_case() == google::bigtable::v2::Value::kArrayValue) { + if (t.kind_case() == google::bigtable::v2::Type::kArrayType) { char const* delimiter = ""; os << '['; for (auto&& val : v.array_value().values()) { @@ -178,15 +234,30 @@ std::ostream& StreamHelper(std::ostream& os, // NOLINT(misc-no-recursion) delimiter = ", "; } return os << ']'; - return os; + } + if (t.kind_case() == google::bigtable::v2::Type::kStructType) { + char const* delimiter = ""; + os << '('; + for (int i = 0; i < v.array_value().values_size(); ++i) { + os << delimiter; + if (!t.struct_type().fields(i).field_name().empty()) { + os << '"'; + EscapeQuotes(os, t.struct_type().fields(i).field_name()); + os << '"' << ": "; + } + StreamHelper(os, v.array_value().values(i), + t.struct_type().fields(i).type(), StreamMode::kAggregate); + delimiter = ", "; + } + return os << ')'; } // this should include type name - return os << "Error: unknown value type code "; + return os << "Error: unknown value type code " << t.kind_case(); } } // namespace bool operator==(Value const& a, Value const& b) { - return Equal(a.type_, a.value_, b.type_, b.value_); + return bigtable::Equal(a.type_, a.value_, b.type_, b.value_); } std::ostream& operator<<(std::ostream& os, Value const& v) { diff --git a/google/cloud/bigtable/value.h b/google/cloud/bigtable/value.h index 2db48ef685353..b8d9ba6ba489a 100644 --- a/google/cloud/bigtable/value.h +++ b/google/cloud/bigtable/value.h @@ -14,6 +14,7 @@ #ifndef GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_BIGTABLE_VALUE_H #define GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_BIGTABLE_VALUE_H +#include "google/cloud/bigtable/internal/tuple_utils.h" #include "google/cloud/bigtable/version.h" #include "google/cloud/internal/make_status.h" #include "google/cloud/status_or.h" @@ -48,6 +49,7 @@ static bool validate_float_value(double v) { static bool ValidateFloatValue(double v) { return validate_float_value(v); } static bool ValidateFloatValue(float v) { return validate_float_value(v); } + GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END } // namespace bigtable_internal @@ -75,6 +77,7 @@ GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN * TIMESTAMP | `google::cloud::bigtable::Timestamp` * DATE | `absl::CivilDay` * ARRAY | `std::vector` // [1] + * STRUCT | `std::tuple` * * [1] The type `T` may be any of the other supported types, except for * ARRAY/`std::vector`. @@ -100,6 +103,30 @@ GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN * assert(vec == copy); * @endcode * + * @par Bigtable Structs + * + * Bigtable structs are represented in C++ as instances of `std::tuple` holding + * zero or more of the allowed Bigtable types, such as `bool`, `std::int64_t`, + * `std::vector`, and even other `std::tuple` objects. Each tuple element + * corresponds to a single field in a Bigtable STRUCT. + * + * Bigtable STRUCT fields may optionally contain a string indicating the field's + * name. Fields names may be empty, unique, or repeated. A named field may be + * specified as a tuple element of type `std::pair`, where the + * pair's `.first` member indicates the field's name, and the `.second` member + * is any valid Bigtable type `T`. + * + * @code + * using Struct = std::tuple>; + * Struct s = {true, {"Foo", 42}}; + * bigtable::Value v(s); + * assert(s == *v.get()); + * @endcode + * + * @note While a STRUCT's (optional) field names are not part of its C++ type, + * they are part of its Bigtable STRUCT type. Array's (i.e., `std::vector`) + * must contain a single element type, therefore it is an error to construct + * a `std::vector` of `std::tuple` objects with differently named fields. */ class Value { public: @@ -176,6 +203,17 @@ class Value { "vector of vector not allowed. See value.h documentation."); } + /** + * Constructs an instance from a Bigtable STRUCT with a type and values + * matching the given `std::tuple`. + * + * Any STRUCT field may optionally have a name, which is specified as + * `std::pair`. + */ + template + explicit Value(std::tuple tup) + : Value(PrivateConstructor{}, std::move(tup)) {} + // Copy and move. Value(Value const&) = default; Value(Value&&) = default; @@ -264,6 +302,32 @@ class Value { return type.has_array_type() && TypeProtoIs(T{}, type.array_type().element_type()); } + template + static bool TypeProtoIs(std::tuple const& tup, + google::bigtable::v2::Type const& type) { + bool ok = type.has_struct_type(); + ok = ok && type.struct_type().fields().size() == sizeof...(Ts); + bigtable_internal::ForEach(tup, IsStructTypeProto{ok, 0}, + type.struct_type()); + return ok; + } + + // A functor to be used with internal::ForEach to check if a StructType proto + // matches the types in a std::tuple. + struct IsStructTypeProto { + bool& ok; + int field; + template + void operator()(T const&, google::bigtable::v2::Type_Struct const& type) { + ok = ok && TypeProtoIs(T{}, type.fields(field).type()); + ++field; + } + template + void operator()(std::pair const&, + google::bigtable::v2::Type_Struct const& type) { + operator()(T{}, type); + } + }; // Tag-dispatch overloads to convert a C++ type to a `Type` protobuf. The // argument type is the tag, the argument value is ignored. @@ -297,6 +361,35 @@ class Value { } return t; } + template + static google::bigtable::v2::Type MakeTypeProto( + std::tuple const& tup) { + google::bigtable::v2::Type t; + t.set_allocated_struct_type( + std::move(new google::bigtable::v2::Type_Struct())); + bigtable_internal::ForEach(tup, AddStructTypes{}, *t.mutable_struct_type()); + return t; + } + + // A functor to be used with internal::ForEach to add type protos for all the + // elements of a tuple. + struct AddStructTypes { + template + void operator()(T const& t, + google::bigtable::v2::Type_Struct& struct_type) const { + auto* field = struct_type.add_fields(); + *field->mutable_type() = MakeTypeProto(t); + } + template < + typename S, typename T, + std::enable_if_t::value, int> = 0> + void operator()(std::pair const& p, + google::bigtable::v2::Type_Struct& struct_type) const { + auto* field = struct_type.add_fields(); + field->set_allocated_field_name(std::move(new std::string(p.first))); + *field->mutable_type() = MakeTypeProto(p.second); + } + }; // Encodes the argument as a protobuf according to the rules described in // https://github.com/googleapis/googleapis/blob/master/google/bigtable/v2/type.proto @@ -327,6 +420,29 @@ class Value { } return v; } + template + static google::bigtable::v2::Value MakeValueProto(std::tuple tup) { + google::bigtable::v2::Value v; + bigtable_internal::ForEach(tup, AddStructValues{}, + *v.mutable_array_value()); + return v; + } + + // A functor to be used with internal::ForEach to add Value protos for all + // the elements of a tuple. + struct AddStructValues { + template + void operator()(T& t, google::bigtable::v2::ArrayValue& list_value) const { + *list_value.add_values() = MakeValueProto(std::move(t)); + } + template < + typename S, typename T, + std::enable_if_t::value, int> = 0> + void operator()(std::pair p, + google::bigtable::v2::ArrayValue& list_value) const { + *list_value.add_values() = MakeValueProto(std::move(p.second)); + } + }; // Tag-dispatch overloads to extract a C++ value from a `Value` protobuf. The // first argument type is the tag, its value is ignored. @@ -368,12 +484,12 @@ class Value { template static StatusOr> GetValue( std::vector const&, V&& pv, google::bigtable::v2::Type const& pt) { - if (pv.kind_case() != google::bigtable::v2::Value::kArrayValue) { + if (!pt.has_array_type() || !pv.has_array_value()) { return internal::UnknownError("missing ARRAY", GCP_ERROR_INFO()); } std::vector v; for (int i = 0; i < pv.array_value().values().size(); ++i) { - auto&& e = GetProtoListValueElement(std::forward(pv), i); + auto&& e = GetProtoValueArrayElement(std::forward(pv), i); using ET = decltype(e); auto value = GetValue(T{}, std::forward(e), pt.array_type().element_type()); @@ -382,16 +498,67 @@ class Value { } return v; } + template + static StatusOr> GetValue( + std::tuple const&, V&& pv, google::bigtable::v2::Type const& pt) { + if (!pt.has_struct_type() || !pv.has_array_value()) { + return internal::UnknownError("missing STRUCT", GCP_ERROR_INFO()); + } + std::tuple tup; + Status status; // OK + ExtractTupleValues f{status, 0, std::forward(pv), pt}; + bigtable_internal::ForEach(tup, f); + if (!status.ok()) return status; + return tup; + } + + // A functor to be used with internal::ForEach to extract C++ types from a + // bigtable::v2::Value proto and with array value store then in a tuple. + template + struct ExtractTupleValues { + Status& status; + int i; + V&& pv; + google::bigtable::v2::Type const& type; + template + void operator()(T& t) { + auto&& e = GetProtoValueArrayElement(std::forward(pv), i); + auto et = type.struct_type().fields(i).type(); + using ET = decltype(e); + auto value = GetValue(T{}, std::forward(e), et); + ++i; + if (!value) { + status = std::move(value).status(); + } else { + t = *std::move(value); + } + } + template + void operator()(std::pair& p) { + p.first = type.struct_type().fields(i).field_name(); + auto&& e = GetProtoValueArrayElement(std::forward(pv), i); + auto et = type.struct_type().fields(i).type(); + using ET = decltype(e); + auto value = GetValue(T{}, std::forward(e), et); + ++i; + if (!value) { + status = std::move(value).status(); + } else { + p.second = *std::move(value); + } + } + }; // Protocol buffers are not friendly to generic programming, because they use // different syntax and different names for mutable and non-mutable - // functions. To make GetValue(vector, ...) (above) work, we need split - // the different protobuf syntaxes into overloaded functions. - static google::bigtable::v2::Value const& GetProtoListValueElement( + // functions. To make GetValue(vector, ...) or GetValue(tuple, ...) + // (above) work, we need split the different protobuf syntaxes into + // overloaded functions. + static google::bigtable::v2::Value const& GetProtoValueArrayElement( google::bigtable::v2::Value const& pv, int pos) { return pv.array_value().values(pos); } - static google::bigtable::v2::Value&& GetProtoListValueElement( + static google::bigtable::v2::Value&& GetProtoValueArrayElement( google::bigtable::v2::Value&& pv, int pos) { return std::move(*pv.mutable_array_value()->mutable_values(pos)); } diff --git a/google/cloud/bigtable/value_test.cc b/google/cloud/bigtable/value_test.cc index 7aeee5a66b5bc..d52fdbc615ccf 100644 --- a/google/cloud/bigtable/value_test.cc +++ b/google/cloud/bigtable/value_test.cc @@ -13,6 +13,7 @@ // limitations under the License. #include "google/cloud/bigtable/value.h" +#include "google/cloud/bigtable/internal/tuple_utils.h" #include "google/cloud/internal/base64_transforms.h" #include "google/cloud/testing_util/is_proto_equal.h" #include "google/cloud/testing_util/status_matchers.h" @@ -36,6 +37,7 @@ GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN namespace { using ::google::cloud::testing_util::IsOk; +using ::google::cloud::testing_util::IsOkAndHolds; using ::google::cloud::testing_util::IsProtoEqual; using ::testing::Not; @@ -194,6 +196,31 @@ TEST(Value, BasicSemantics) { } } +TEST(Value, ArrayValueBasedEquality) { + std::vector test_cases = { + Value(std::vector{1.2, 3.4}), + Value(std::make_tuple(1.2, 3.4)), + + // empty containers + Value(std::vector()), + Value(std::make_tuple()), + }; + + for (size_t i = 0; i < test_cases.size(); i++) { + auto const& tc1 = test_cases[i]; + for (size_t j = 0; j < test_cases.size(); j++) { + auto const& tc2 = test_cases[j]; + // Compares tc1 to tc2, which ensures that different "kinds" of + // value are never equal. + if (i == j) { + EXPECT_EQ(tc1, tc2); + continue; + } + EXPECT_NE(tc1, tc2); + } + } +} + TEST(Value, Equality) { std::vector> test_cases = { {Value(false), Value(true)}, @@ -207,6 +234,8 @@ TEST(Value, Equality) { {Value(absl::CivilDay(1970, 1, 1)), Value(absl::CivilDay(2020, 3, 15))}, {Value(std::vector{1.2, 3.4}), Value(std::vector{4.5, 6.7})}, + {Value(std::make_tuple(false, 123, "foo")), + Value(std::make_tuple(true, 456, "bar"))}, }; for (auto const& tc : test_cases) { @@ -320,6 +349,32 @@ TEST(Value, RvalueGetVectorString) { EXPECT_EQ(Type(data.size(), ""), *s); } +// NOTE: This test relies on unspecified behavior about the moved-from state +// of std::string. Specifically, this test relies on the fact that "large" +// strings, when moved-from, end up empty. And we use this fact to verify that +// spanner::Value::get() correctly handles moves. If this test ever breaks +// on some platform, we could probably delete this, unless we can think of a +// better way to test move semantics. +TEST(Value, RvalueGetStructString) { + using Type = std::tuple, std::string>; + Type data{std::make_pair("name", std::string(128, 'x')), + std::string(128, 'x')}; + Value v(data); + + auto s = v.get(); + ASSERT_STATUS_OK(s); + EXPECT_EQ(data, *s); + + s = std::move(v).get(); + ASSERT_STATUS_OK(s); + EXPECT_EQ(data, *s); + + // NOLINTNEXTLINE(bugprone-use-after-move) + s = MovedFromString(v); + ASSERT_STATUS_OK(s); + EXPECT_EQ(Type({"name", ""}, ""), *s); +} + TEST(Value, BytesRelationalOperators) { Bytes b1(std::string(1, '\x00')); Bytes b2(std::string(1, '\xff')); @@ -438,6 +493,123 @@ TEST(Value, BigtableArray) { EXPECT_THAT(null_vf.get(), Not(IsOk())); } +TEST(Value, BigtableStruct) { + // Using declarations to shorten the tests, making them more readable. + using std::int64_t; + using std::make_pair; + using std::make_tuple; + using std::pair; + using std::string; + using std::tuple; + + auto tup1 = make_tuple(false, int64_t{123}); + using T1 = decltype(tup1); + Value v1(tup1); + ASSERT_STATUS_OK(v1.get()); + EXPECT_EQ(tup1, *v1.get()); + EXPECT_EQ(v1, v1); + + // Verify we can extract tuple elements even if they're wrapped in a pair. + auto const pair0 = v1.get, int64_t>>(); + ASSERT_STATUS_OK(pair0); + EXPECT_EQ(std::get<0>(tup1), std::get<0>(*pair0).second); + EXPECT_EQ(std::get<1>(tup1), std::get<1>(*pair0)); + auto const pair1 = v1.get>>(); + ASSERT_STATUS_OK(pair1); + EXPECT_EQ(std::get<0>(tup1), std::get<0>(*pair1)); + EXPECT_EQ(std::get<1>(tup1), std::get<1>(*pair1).second); + auto const pair01 = + v1.get, pair>>(); + ASSERT_STATUS_OK(pair01); + EXPECT_EQ(std::get<0>(tup1), std::get<0>(*pair01).second); + EXPECT_EQ(std::get<1>(tup1), std::get<1>(*pair01).second); + + auto tup2 = make_tuple(false, make_pair(string("f2"), int64_t{123})); + using T2 = decltype(tup2); + Value v2(tup2); + EXPECT_THAT(v2.get(), IsOkAndHolds(tup2)); + EXPECT_EQ(v2, v2); + EXPECT_NE(v2, v1); + + // T1 is lacking field names, but otherwise the same as T2. + EXPECT_EQ(tup1, *v2.get()); + EXPECT_NE(tup2, *v1.get()); + + auto tup3 = make_tuple(false, make_pair(string("Other"), int64_t{123})); + using T3 = decltype(tup3); + Value v3(tup3); + EXPECT_THAT(v3.get(), IsOkAndHolds(tup3)); + EXPECT_EQ(v3, v3); + EXPECT_NE(v3, v2); + EXPECT_NE(v3, v1); + + static_assert(std::is_same::value, "Only diff is field name"); + + // v1 != v2, yet T2 works with v1 and vice versa + EXPECT_NE(v1, v2); + EXPECT_STATUS_OK(v1.get()); + EXPECT_STATUS_OK(v2.get()); + + Value v_null(absl::optional{}); + EXPECT_FALSE(v_null.get>()->has_value()); + EXPECT_FALSE(v_null.get>()->has_value()); + + EXPECT_NE(v1, v_null); + EXPECT_NE(v2, v_null); + + auto array_struct = std::vector{ + T3{false, {"age", 1}}, + T3{true, {"age", 2}}, + T3{false, {"age", 3}}, + }; + using T4 = decltype(array_struct); + Value v4(array_struct); + EXPECT_STATUS_OK(v4.get()); + EXPECT_THAT(v4.get(), Not(IsOk())); + EXPECT_THAT(v4.get(), Not(IsOk())); + EXPECT_THAT(v4.get(), Not(IsOk())); + + EXPECT_THAT(v4.get(), IsOkAndHolds(array_struct)); + + auto empty = tuple<>{}; + using T5 = decltype(empty); + Value v5(empty); + EXPECT_STATUS_OK(v5.get()); + EXPECT_THAT(v5.get(), Not(IsOk())); + EXPECT_EQ(v5, v5); + EXPECT_NE(v5, v4); + + EXPECT_THAT(v5.get(), IsOkAndHolds(empty)); + + auto deeply_nested = tuple>>>{}; + using T6 = decltype(deeply_nested); + Value v6(deeply_nested); + EXPECT_STATUS_OK(v6.get()); + EXPECT_THAT(v6.get(), Not(IsOk())); + EXPECT_EQ(v6, v6); + EXPECT_NE(v6, v5); + + EXPECT_THAT(v6.get(), IsOkAndHolds(deeply_nested)); +} + +TEST(Value, BigtableStructWithNull) { + auto v1 = Value(std::make_tuple(123, true)); + auto v2 = Value(std::make_tuple(123, absl::optional{})); + + auto protos1 = bigtable_internal::ToProto(v1); + auto protos2 = bigtable_internal::ToProto(v2); + + // The type protos match for both values, but the value protos DO NOT match. + EXPECT_THAT(protos1.first, IsProtoEqual(protos2.first)); + EXPECT_THAT(protos1.second, Not(IsProtoEqual(protos2.second))); + + // Now verify that the second value has two fields and the second field + // contains a NULL value. + ASSERT_EQ(protos2.second.array_value().values_size(), 2); + ASSERT_EQ(protos2.second.array_value().values(1).kind_case(), + google::bigtable::v2::Value::KIND_NOT_SET); +} + TEST(Value, ProtoConversionBool) { for (auto b : {true, false}) { Value const v(b); @@ -577,6 +749,29 @@ TEST(Value, ProtoConversionArray) { EXPECT_EQ(3, p.second.array_value().values(2).int_value()); } +TEST(Value, ProtoConversionStruct) { + auto data = std::make_tuple(3.14, std::make_pair("foo", 42)); + Value const v(data); + auto const p = bigtable_internal::ToProto(v); + EXPECT_EQ(v, bigtable_internal::FromProto(p.first, p.second)); + EXPECT_TRUE(p.first.has_struct_type()); + + Value const null_struct_value( + MakeNullValue>()); + auto const null_struct_proto = bigtable_internal::ToProto(null_struct_value); + EXPECT_TRUE(p.first.has_struct_type()); + + auto const& field0 = p.first.struct_type().fields(0); + EXPECT_EQ("", field0.field_name()); + EXPECT_TRUE(field0.type().has_float64_type()); + EXPECT_EQ(3.14, p.second.array_value().values(0).float_value()); + + auto const& field1 = p.first.struct_type().fields(1); + EXPECT_EQ("foo", field1.field_name()); + EXPECT_TRUE(field1.type().has_int64_type()); + EXPECT_EQ(42, p.second.array_value().values(1).int_value()); +} + void SetNullProtoKind(Value& v) { auto p = bigtable_internal::ToProto(v); p.second.clear_kind(); @@ -608,6 +803,31 @@ void SetProtoKind(Value& v, char const* x) { v = bigtable_internal::FromProto(p.first, p.second); } +void SetProtoKind(Value& v, std::vector const& x) { + auto p = bigtable_internal::ToProto(v); + auto list = *p.second.mutable_array_value(); + for (auto&& e : x) { + google::bigtable::v2::Value el; + el.set_int_value(e); + *list.add_values() = el; + } + v = bigtable_internal::FromProto(p.first, p.second); +} + +void SetProtoKind(Value& v, std::tuple const& x) { + auto p = bigtable_internal::ToProto(v); + auto list = *p.second.mutable_array_value(); + auto e = std::get<0>(x); + google::bigtable::v2::Value el; + el.set_int_value(e); + *list.add_values() = el; + el = google::bigtable::v2::Value(); + e = std::get<1>(x); + el.set_int_value(e); + *list.add_values() = el; + v = bigtable_internal::FromProto(p.first, p.second); +} + void ClearProtoKind(Value& v) { auto p = bigtable_internal::ToProto(v); p.first.clear_kind(); @@ -631,6 +851,12 @@ TEST(Value, GetBadBool) { SetProtoKind(v, "hello"); EXPECT_THAT(v.get(), Not(IsOk())); + + SetProtoKind(v, std::vector{1, 2}); + EXPECT_THAT(v.get(), Not(IsOk())); + + SetProtoKind(v, std::make_tuple(1, 2)); + EXPECT_THAT(v.get(), Not(IsOk())); } TEST(Value, GetBadFloat64) { @@ -658,6 +884,12 @@ TEST(Value, GetBadFloat64) { SetProtoKind(v, std::nan("NaN")); EXPECT_THAT(v.get(), Not(IsOk())); + + SetProtoKind(v, std::vector{1, 2}); + EXPECT_THAT(v.get(), Not(IsOk())); + + SetProtoKind(v, std::make_tuple(1, 2)); + EXPECT_THAT(v.get(), Not(IsOk())); } TEST(Value, GetBadFloat32) { @@ -685,6 +917,12 @@ TEST(Value, GetBadFloat32) { SetProtoKind(v, std::nan("NaN")); EXPECT_THAT(v.get(), Not(IsOk())); + + SetProtoKind(v, std::vector{1, 2}); + EXPECT_THAT(v.get(), Not(IsOk())); + + SetProtoKind(v, std::make_tuple(1, 2)); + EXPECT_THAT(v.get(), Not(IsOk())); } TEST(Value, GetBadString) { @@ -700,6 +938,12 @@ TEST(Value, GetBadString) { SetProtoKind(v, 0.0); EXPECT_THAT(v.get(), Not(IsOk())); + + SetProtoKind(v, std::vector{1, 2}); + EXPECT_THAT(v.get(), Not(IsOk())); + + SetProtoKind(v, std::make_tuple(1, 2)); + EXPECT_THAT(v.get(), Not(IsOk())); } TEST(Value, GetBadBytes) { @@ -715,6 +959,12 @@ TEST(Value, GetBadBytes) { SetProtoKind(v, 0.0); EXPECT_THAT(v.get(), Not(IsOk())); + + SetProtoKind(v, std::vector{1, 2}); + EXPECT_THAT(v.get(), Not(IsOk())); + + SetProtoKind(v, std::make_tuple(1, 2)); + EXPECT_THAT(v.get(), Not(IsOk())); } TEST(Value, GetBadTimestamp) { @@ -733,6 +983,12 @@ TEST(Value, GetBadTimestamp) { SetProtoKind(v, "blah"); EXPECT_THAT(v.get(), Not(IsOk())); + + SetProtoKind(v, std::vector{1, 2}); + EXPECT_THAT(v.get(), Not(IsOk())); + + SetProtoKind(v, std::make_tuple(1, 2)); + EXPECT_THAT(v.get(), Not(IsOk())); } TEST(Value, GetBadDate) { @@ -751,6 +1007,12 @@ TEST(Value, GetBadDate) { SetProtoKind(v, "blah"); EXPECT_THAT(v.get(), Not(IsOk())); + + SetProtoKind(v, std::vector{1, 2}); + EXPECT_THAT(v.get(), Not(IsOk())); + + SetProtoKind(v, std::make_tuple(1, 2)); + EXPECT_THAT(v.get(), Not(IsOk())); } TEST(Value, GetBadOptional) { @@ -763,6 +1025,12 @@ TEST(Value, GetBadOptional) { SetProtoKind(v, "blah"); EXPECT_THAT(v.get>(), Not(IsOk())); + + SetProtoKind(v, std::vector{1, 2}); + EXPECT_THAT(v.get>(), Not(IsOk())); + + SetProtoKind(v, std::make_tuple(1, 2)); + EXPECT_THAT(v.get>(), Not(IsOk())); } TEST(Value, GetBadArray) { @@ -781,6 +1049,36 @@ TEST(Value, GetBadArray) { SetProtoKind(v, "blah"); EXPECT_THAT(v.get>(), Not(IsOk())); + + SetProtoKind(v, std::vector{1, 2}); + EXPECT_THAT(v.get>(), Not(IsOk())); + + SetProtoKind(v, std::make_tuple(1, 2)); + EXPECT_THAT(v.get>(), Not(IsOk())); +} + +TEST(Value, GetBadStruct) { + Value v(std::tuple{}); + ClearProtoKind(v); + EXPECT_THAT(v.get>(), Not(IsOk())); + + SetNullProtoKind(v); + EXPECT_THAT(v.get>(), Not(IsOk())); + + SetProtoKind(v, true); + EXPECT_THAT(v.get>(), Not(IsOk())); + + SetProtoKind(v, 0.0); + EXPECT_THAT(v.get>(), Not(IsOk())); + + SetProtoKind(v, "blah"); + EXPECT_THAT(v.get>(), Not(IsOk())); + + SetProtoKind(v, std::vector{1, 2}); + EXPECT_THAT(v.get>(), Not(IsOk())); + + SetProtoKind(v, std::make_tuple(1, 2)); + EXPECT_THAT(v.get>(), Not(IsOk())); } TEST(Value, OutputStream) { @@ -794,6 +1092,9 @@ TEST(Value, OutputStream) { auto const float4 = [](std::ostream& os) -> std::ostream& { return os << std::showpoint << std::setprecision(4); }; + auto const alphahex = [](std::ostream& os) -> std::ostream& { + return os << std::boolalpha << std::hex; + }; struct TestCase { Value value; @@ -863,7 +1164,47 @@ TEST(Value, OutputStream) { {MakeNullValue>(), "NULL", normal}, {MakeNullValue>(), "NULL", normal}, {MakeNullValue>(), "NULL", normal}, - {MakeNullValue>(), "NULL", normal}}; + {MakeNullValue>(), "NULL", normal}, + + // Tests structs + {Value(std::make_tuple(true, 123)), "(1, 123)", normal}, + {Value(std::make_tuple(true, 123)), "(true, 7b)", alphahex}, + {Value(std::make_tuple(std::make_pair("A", true), + std::make_pair("B", 123))), + R"(("A": 1, "B": 123))", normal}, + {Value(std::make_tuple(std::make_pair("A", true), + std::make_pair("B", 123))), + R"(("A": true, "B": 7b))", alphahex}, + {Value(std::make_tuple( + std::vector{10, 11, 12}, + std::make_pair("B", std::vector{13, 14, 15}))), + R"(([10, 11, 12], "B": [13, 14, 15]))", normal}, + {Value(std::make_tuple( + std::vector{10, 11, 12}, + std::make_pair("B", std::vector{13, 14, 15}))), + R"(([a, b, c], "B": [d, e, f]))", hex}, + {Value(std::make_tuple(std::make_tuple( + std::make_tuple(std::vector{10, 11, 12})))), + "((([10, 11, 12])))", normal}, + {Value(std::make_tuple(std::make_tuple( + std::make_tuple(std::vector{10, 11, 12})))), + "((([a, b, c])))", hex}, + + // Tests struct with null members + {Value(std::make_tuple(absl::optional{})), "(NULL)", normal}, + {Value(std::make_tuple(absl::optional{}, 123)), "(NULL, 123)", + normal}, + {Value(std::make_tuple(absl::optional{}, 123)), "(NULL, 7b)", hex}, + {Value(std::make_tuple(absl::optional{}, + absl::optional{})), + "(NULL, NULL)", normal}, + + // Tests null structs + {MakeNullValue>(), "NULL", normal}, + {MakeNullValue>(), "NULL", normal}, + {MakeNullValue>(), "NULL", normal}, + {MakeNullValue>(), "NULL", normal}, + }; for (auto const& tc : test_case) { std::stringstream ss;