diff --git a/google/cloud/spanner/integration_tests/data_types_integration_test.cc b/google/cloud/spanner/integration_tests/data_types_integration_test.cc index 346408984620..7232be0bec1d 100644 --- a/google/cloud/spanner/integration_tests/data_types_integration_test.cc +++ b/google/cloud/spanner/integration_tests/data_types_integration_test.cc @@ -50,7 +50,7 @@ StatusOr WriteReadData(Client& client, T const& data, int id = 0; for (auto&& x : data) { mutations.push_back(MakeInsertMutation("DataTypes", {"Id", column}, - "Id-" + std::to_string(id++), x)); + "Id-" + std::to_string(++id), x)); } auto commit_result = client.Commit(std::move(mutations)); if (!commit_result) return commit_result.status(); @@ -241,6 +241,20 @@ TEST_F(DataTypeIntegrationTest, WriteReadJson) { EXPECT_THAT(*result, UnorderedElementsAreArray(data)); } +TEST_F(PgDataTypeIntegrationTest, WriteReadJson) { + if (UsingEmulator()) GTEST_SKIP() << "emulator does not support PostgreSQL"; + + std::vector const data = { + JsonB(), // + JsonB(R"("Hello world!")"), // + JsonB("42"), // + JsonB("true"), // + }; + auto result = WriteReadData(*client_, data, "JsonValue"); + ASSERT_STATUS_OK(result); + EXPECT_THAT(*result, UnorderedElementsAreArray(data)); +} + TEST_F(DataTypeIntegrationTest, WriteReadNumeric) { auto min = MakeNumeric("-99999999999999999999999999999.999999999"); ASSERT_STATUS_OK(min); @@ -387,6 +401,33 @@ TEST_F(DataTypeIntegrationTest, WriteReadArrayJson) { EXPECT_THAT(*result, UnorderedElementsAreArray(data)); } +TEST_F(PgDataTypeIntegrationTest, WriteReadArrayJson) { + if (UsingEmulator()) GTEST_SKIP() << "emulator does not support PostgreSQL"; + + std::vector> const data = { + std::vector{}, + std::vector{JsonB()}, + std::vector{ + JsonB(), + JsonB(R"("Hello world!")"), + JsonB("42"), + JsonB("true"), + }, + }; + auto result = WriteReadData(*client_, data, "ArrayJsonValue"); + { + // TODO(#10095): Remove this when JSONB[] is supported. + auto matcher = testing_util::StatusIs( + StatusCode::kNotFound, testing::HasSubstr("Column not found in table")); + testing::StringMatchResultListener listener; + if (matcher.impl().MatchAndExplain(result, &listener)) { + GTEST_SKIP(); + } + } + ASSERT_STATUS_OK(result); + EXPECT_THAT(*result, UnorderedElementsAreArray(data)); +} + TEST_F(DataTypeIntegrationTest, WriteReadArrayNumeric) { std::vector> const data = { std::vector{}, @@ -432,8 +473,12 @@ TEST_F(DataTypeIntegrationTest, JsonIndexAndPrimaryKey) { auto metadata = admin_client.UpdateDatabaseDdl(GetDatabase().FullName(), statements) .get(); - EXPECT_THAT(metadata, StatusIs(StatusCode::kFailedPrecondition, - HasSubstr("DataTypesByJsonValue"))); + EXPECT_THAT(metadata, + StatusIs(StatusCode::kFailedPrecondition, + AnyOf(AllOf(HasSubstr("Index DataTypesByJsonValue"), + HasSubstr("unsupported type JSON")), + AllOf(HasSubstr("index DataTypesByJsonValue"), + HasSubstr("Cannot reference JSON"))))); // Verify that a JSON column cannot be used as a primary key. statements.clear(); @@ -446,10 +491,75 @@ TEST_F(DataTypeIntegrationTest, JsonIndexAndPrimaryKey) { admin_client.UpdateDatabaseDdl(GetDatabase().FullName(), statements) .get(); EXPECT_THAT(metadata, StatusIs(StatusCode::kInvalidArgument, - AllOf(HasSubstr("Key has type JSON"), + AllOf(HasSubstr("has type JSON"), + HasSubstr("part of the primary key")))); +} + +TEST_F(PgDataTypeIntegrationTest, JsonIndexAndPrimaryKey) { + if (UsingEmulator()) GTEST_SKIP() << "emulator does not support PostgreSQL"; + + spanner_admin::DatabaseAdminClient admin_client( + spanner_admin::MakeDatabaseAdminConnection()); + + // Verify that a JSONB column cannot be used as an index. + std::vector statements; + statements.emplace_back(R"""( + CREATE INDEX DataTypesByJsonValue + ON DataTypes(JsonValue) + )"""); + auto metadata = + admin_client.UpdateDatabaseDdl(GetDatabase().FullName(), statements) + .get(); + EXPECT_THAT(metadata, + StatusIs(StatusCode::kFailedPrecondition, + AllOf(HasSubstr("Index datatypesbyjsonvalue"), + HasSubstr("unsupported type PG.JSONB")))); + + // Verify that a JSONB column cannot be used as a primary key. + statements.clear(); + statements.emplace_back(R"""( + CREATE TABLE JsonKey ( + Key JSONB NOT NULL, + PRIMARY KEY (Key) + ) + )"""); + metadata = + admin_client.UpdateDatabaseDdl(GetDatabase().FullName(), statements) + .get(); + EXPECT_THAT(metadata, StatusIs(StatusCode::kInvalidArgument, + AllOf(HasSubstr("has type PG.JSONB"), HasSubstr("part of the primary key")))); } +TEST_F(PgDataTypeIntegrationTest, InsertAndQueryWithJson) { + if (UsingEmulator()) GTEST_SKIP() << "emulator does not support PostgreSQL"; + + auto& client = *client_; + auto commit_result = + client.Commit([&client](Transaction const& txn) -> StatusOr { + auto dml_result = client.ExecuteDml( + txn, + SqlStatement("INSERT INTO DataTypes (Id, JsonValue)" + " VALUES($1, $2)", + {{"p1", Value("Id-1")}, {"p2", Value(JsonB("42"))}})); + if (!dml_result) return dml_result.status(); + return Mutations{}; + }); + ASSERT_STATUS_OK(commit_result); + + auto rows = + client_->ExecuteQuery(SqlStatement("SELECT Id, JsonValue FROM DataTypes" + " WHERE Id = $1", + {{"p1", Value("Id-1")}})); + using RowType = std::tuple>; + auto row = GetSingularRow(StreamOf(rows)); + ASSERT_STATUS_OK(row); + + auto const& v = std::get<1>(*row); + ASSERT_TRUE(v.has_value()); + EXPECT_EQ(*v, JsonB("42")); +} + TEST_F(DataTypeIntegrationTest, InsertAndQueryWithNumericKey) { auto& client = *client_; auto const key = MakeNumeric(42).value(); @@ -513,8 +623,8 @@ TEST_F(DataTypeIntegrationTest, InsertAndQueryWithStruct) { txn, SqlStatement( "INSERT INTO DataTypes (Id, StringValue, ArrayInt64Value)" - "VALUES(@id, @struct.StringValue, @struct.ArrayInt64Value)", - {{"id", Value("id-1")}, {"struct", Value(data)}})); + " VALUES(@id, @struct.StringValue, @struct.ArrayInt64Value)", + {{"id", Value("Id-1")}, {"struct", Value(data)}})); if (!dml_result) return dml_result.status(); return Mutations{}; }); diff --git a/google/cloud/spanner/internal/connection_impl_test.cc b/google/cloud/spanner/internal/connection_impl_test.cc index 3322d0413441..21e4cbd19c9e 100644 --- a/google/cloud/spanner/internal/connection_impl_test.cc +++ b/google/cloud/spanner/internal/connection_impl_test.cc @@ -853,6 +853,53 @@ TEST(ConnectionImplTest, ExecuteQueryPgNumericResult) { EXPECT_EQ(row_number, expected.size()); } +TEST(ConnectionImplTest, ExecuteQueryJsonBResult) { + auto mock = std::make_shared(); + auto db = spanner::Database("placeholder_project", "placeholder_instance", + "placeholder_database_id"); + EXPECT_CALL(*mock, BatchCreateSessions(_, HasDatabase(db))) + .WillOnce(Return(MakeSessionsResponse({"test-session-name"}))); + auto constexpr kText = R"pb( + metadata: { + row_type: { + fields: { + name: "ColumnA", + type: { code: JSON type_annotation: PG_JSONB } + } + fields: { + name: "ColumnB", + type: { code: JSON type_annotation: PG_JSONB } + } + } + } + values: { string_value: "42" } + values: { null_value: NULL_VALUE } + values: { string_value: "[null, null]" } + values: { string_value: "{\"a\": 1, \"b\": 2}" } + )pb"; + EXPECT_CALL(*mock, ExecuteStreamingSql) + .WillOnce(Return(ByMove(MakeReader({kText})))); + + auto conn = MakeConnectionImpl(db, mock); + internal::OptionsSpan span(MakeLimitedTimeOptions()); + auto rows = conn->ExecuteQuery( + {MakeSingleUseTransaction(spanner::Transaction::ReadOnlyOptions()), + spanner::SqlStatement("SELECT * FROM Table")}); + using RowType = std::tuple>; + auto expected = std::vector{ + RowType(spanner::JsonB("42"), absl::nullopt), + RowType(spanner::JsonB("[null, null]"), + spanner::JsonB(R"({"a": 1, "b": 2})")), + }; + int row_number = 0; + for (auto& row : spanner::StreamOf(rows)) { + EXPECT_STATUS_OK(row); + EXPECT_EQ(*row, expected[row_number]); + ++row_number; + } + EXPECT_EQ(row_number, expected.size()); +} + TEST(ConnectionImplTest, ExecuteQueryNumericParameter) { auto mock = std::make_shared(); auto db = spanner::Database("placeholder_project", "placeholder_instance", diff --git a/google/cloud/spanner/json.h b/google/cloud/spanner/json.h index 29de6dea57ca..ca24f28d5c21 100644 --- a/google/cloud/spanner/json.h +++ b/google/cloud/spanner/json.h @@ -81,6 +81,68 @@ inline std::ostream& operator<<(std::ostream& os, Json const& json) { return os << std::string(json); } +/** + * `JsonB` is a variant of `Json` (see above). While both classes share the + * same, thin client-side API, `JsonB` stores the data in a decomposed, + * binary format, whereas `Json` stores an exact copy of the RFC 7159 text. + * + * This means that `JsonB` is slower to input, but faster to process as it + * avoids reparsing. Therefore, applications that utilize the structured + * state of a JSON value should prefer `JsonB`. + * + * It also means that the `JsonB` stored representation does NOT preserve: + * - white space, + * - the order of object keys, or + * - duplicate object keys. + * + * Note: `JsonB` is only applicable to PostgreSQL databases (i.e., those + * created using `DatabaseDialect::POSTGRESQL`). + */ +class JsonB { + public: + /// A null value. + JsonB() : rep_("null") {} + + /// Regular value type, supporting copy, assign, move. + ///@{ + JsonB(JsonB const&) = default; + JsonB& operator=(JsonB const&) = default; + JsonB(JsonB&&) = default; + JsonB& operator=(JsonB&&) = default; + ///@} + + /** + * Construction from a JSON-formatted string. Note that there is no check + * here that the argument string is indeed well-formatted. Error detection + * will be delayed until the value is passed to Spanner. + */ + explicit JsonB(std::string s) : rep_(std::move(s)) {} + + /// Conversion to a JSON-formatted string. + ///@{ + explicit operator std::string() const& { return rep_; } + explicit operator std::string() && { return std::move(rep_); } + ///@} + + private: + std::string rep_; // a (presumably) JSON-formatted string +}; + +/// @name Relational operators +///@{ +inline bool operator==(JsonB const& lhs, JsonB const& rhs) { + return std::string(lhs) == std::string(rhs); +} +inline bool operator!=(JsonB const& lhs, JsonB const& rhs) { + return !(lhs == rhs); +} +///@} + +/// Outputs a JSON-formatted string to the provided stream. +inline std::ostream& operator<<(std::ostream& os, JsonB const& json) { + return os << std::string(json); +} + GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END } // namespace spanner } // namespace cloud diff --git a/google/cloud/spanner/samples/postgresql_samples.cc b/google/cloud/spanner/samples/postgresql_samples.cc index 0fcbdf878ddb..5940c0814265 100644 --- a/google/cloud/spanner/samples/postgresql_samples.cc +++ b/google/cloud/spanner/samples/postgresql_samples.cc @@ -525,7 +525,7 @@ void InformationSchema( R"""( CREATE TABLE Venues ( VenueId BIGINT NOT NULL PRIMARY KEY, - Name CHARACTER VARYING(1024) NOT NULL, + Name CHARACTER VARYING(1024), Revenue NUMERIC, Picture BYTEA ) @@ -690,6 +690,81 @@ void PartitionedDml(google::cloud::spanner::Client client) { } // [END spanner_postgresql_partitioned_dml] +// [START spanner_postgresql_jsonb_add_column] +void JsonbAddColumn(google::cloud::spanner_admin::DatabaseAdminClient client, + google::cloud::spanner::Database const& database) { + std::vector statements = { + R"""( + ALTER TABLE Venues + ADD COLUMN VenueDetails JSONB + )""", + }; + auto metadata = + client.UpdateDatabaseDdl(database.FullName(), statements).get(); + google::cloud::spanner_testing::LogUpdateDatabaseDdl( //! TODO(#4758) + client, database, metadata.status()); //! TODO(#4758) + if (!metadata) throw std::move(metadata).status(); + std::cout << "Added JSONB column to table Venues in database " + << database.FullName() << "\nNew DDL:\n" + << metadata->DebugString(); +} +// [END spanner_postgresql_jsonb_add_column] + +// [START spanner_postgresql_jsonb_update_data] +void JsonbUpdateData(google::cloud::spanner::Client client) { + auto venue19_details = google::cloud::spanner::JsonB(R"""( + {"rating": 9, "open": true} + )"""); + // PG.JSONB takes the last value in the case of duplicate keys. + auto venue4_details = google::cloud::spanner::JsonB(R"""( + [ + {"name": null, "available": true}, + {"name": "room 2", "available": false, "name": "room 3"}, + { + "main hall": { + "description": "this is the biggest space", + "size": 200 + } + } + ] + )"""); + auto venue42_details = google::cloud::spanner::JsonB(R"""( + { + "name": null, + "open": {"Monday": true, "Tuesday": false}, + "tags": ["large", "airy"] + } + )"""); + auto update_venues = google::cloud::spanner::InsertOrUpdateMutationBuilder( + "Venues", {"VenueId", "VenueDetails"}) + .EmplaceRow(19, venue19_details) + .EmplaceRow(4, venue4_details) + .EmplaceRow(42, venue42_details) + .Build(); + auto commit_result = + client.Commit(google::cloud::spanner::Mutations{update_venues}); + if (!commit_result) throw std::move(commit_result).status(); + std::cout << "Updated data.\n"; +} +// [END spanner_postgresql_jsonb_update_data] + +// [START spanner_postgresql_jsonb_query_parameter] +void JsonbQueryWithParameter(google::cloud::spanner::Client client) { + auto sql = google::cloud::spanner::SqlStatement( + "SELECT VenueId, VenueDetails FROM Venues" + " WHERE CAST(VenueDetails ->> 'rating' AS INTEGER) > $1", + {{"p1", google::cloud::spanner::Value(2)}}); + using RowType = + std::tuple>; + auto rows = client.ExecuteQuery(std::move(sql)); + for (auto& row : google::cloud::spanner::StreamOf(rows)) { + if (!row) throw std::move(row).status(); + std::cout << "VenueId: " << std::get<0>(*row) << ", "; + std::cout << "Details: " << std::string(std::get<1>(*row).value()) << "\n"; + } +} +// [END spanner_postgresql_jsonb_query_parameter] + void DropDatabase(google::cloud::spanner_admin::DatabaseAdminClient client, google::cloud::spanner::Database const& database) { auto status = client.DropDatabase(database.FullName()); @@ -842,6 +917,9 @@ int RunOneCommand(std::vector argv, {"numeric-data-type", Command(samples::NumericDataType)}, {"information-schema", Command(samples::InformationSchema)}, {"partitioned-dml", Command(samples::PartitionedDml)}, + {"jsonb-add-column", Command(samples::JsonbAddColumn)}, + {"jsonb-update-data", Command(samples::JsonbUpdateData)}, + {"jsonb-query-with-parameter", Command(samples::JsonbQueryWithParameter)}, {"drop-database", Command(samples::DropDatabase)}, {"help", HelpCommand(commands)}, }; @@ -949,6 +1027,15 @@ int RunAll() { SampleBanner("spanner_postgresql_partitioned_dml"); samples::PartitionedDml(client); + + SampleBanner("spanner_postgresql_jsonb_add_column"); + samples::JsonbAddColumn(database_admin_client, database); + + SampleBanner("spanner_postgresql_jsonb_update_data"); + samples::JsonbUpdateData(client); + + SampleBanner("spanner_postgresql_jsonb_query_parameter"); + samples::JsonbQueryWithParameter(client); } catch (...) { // Try to clean up after a failure. samples::DropDatabase(database_admin_client, database); diff --git a/google/cloud/spanner/testing/database_integration_test.cc b/google/cloud/spanner/testing/database_integration_test.cc index 8c2abd52a3b2..e106d9f72dbb 100644 --- a/google/cloud/spanner/testing/database_integration_test.cc +++ b/google/cloud/spanner/testing/database_integration_test.cc @@ -207,6 +207,7 @@ void PgDatabaseIntegrationTest::SetUpTestSuite() { BytesValue BYTEA, TimestampValue TIMESTAMP WITH TIME ZONE, DateValue DATE, + JsonValue JSONB, NumericValue NUMERIC, ArrayBoolValue BOOLEAN[], ArrayInt64Value BIGINT[], @@ -215,6 +216,7 @@ void PgDatabaseIntegrationTest::SetUpTestSuite() { ArrayBytesValue BYTEA[], ArrayTimestampValue TIMESTAMP WITH TIME ZONE[], ArrayDateValue DATE[], + -- TODO(#10095): ArrayJsonValue JSONB[], ArrayNumericValue NUMERIC[], PRIMARY KEY(Id) ) diff --git a/google/cloud/spanner/value.cc b/google/cloud/spanner/value.cc index b3ea4f24e947..8d3a4f8b44d4 100644 --- a/google/cloud/spanner/value.cc +++ b/google/cloud/spanner/value.cc @@ -235,7 +235,15 @@ bool Value::TypeProtoIs(Bytes const&, google::spanner::v1::Type const& type) { } bool Value::TypeProtoIs(Json const&, google::spanner::v1::Type const& type) { - return type.code() == google::spanner::v1::TypeCode::JSON; + return type.code() == google::spanner::v1::TypeCode::JSON && + type.type_annotation() == google::spanner::v1::TypeAnnotationCode:: + TYPE_ANNOTATION_CODE_UNSPECIFIED; +} + +bool Value::TypeProtoIs(JsonB const&, google::spanner::v1::Type const& type) { + return type.code() == google::spanner::v1::TypeCode::JSON && + type.type_annotation() == + google::spanner::v1::TypeAnnotationCode::PG_JSONB; } bool Value::TypeProtoIs(Numeric const&, google::spanner::v1::Type const& type) { @@ -288,6 +296,15 @@ google::spanner::v1::Type Value::MakeTypeProto(Bytes const&) { google::spanner::v1::Type Value::MakeTypeProto(Json const&) { google::spanner::v1::Type t; t.set_code(google::spanner::v1::TypeCode::JSON); + // Prefer to leave type_annotation unset over setting it to + // TypeAnnotationCode::TYPE_ANNOTATION_CODE_UNSPECIFIED. + return t; +} + +google::spanner::v1::Type Value::MakeTypeProto(JsonB const&) { + google::spanner::v1::Type t; + t.set_code(google::spanner::v1::TypeCode::JSON); + t.set_type_annotation(google::spanner::v1::TypeAnnotationCode::PG_JSONB); return t; } @@ -378,6 +395,12 @@ google::protobuf::Value Value::MakeValueProto(Json j) { return v; } +google::protobuf::Value Value::MakeValueProto(JsonB j) { + google::protobuf::Value v; + v.set_string_value(std::string(std::move(j))); + return v; +} + google::protobuf::Value Value::MakeValueProto(Numeric n) { google::protobuf::Value v; v.set_string_value(std::move(n).ToString()); @@ -509,6 +532,14 @@ StatusOr Value::GetValue(Json const&, google::protobuf::Value const& pv, return Json(pv.string_value()); } +StatusOr Value::GetValue(JsonB const&, google::protobuf::Value const& pv, + google::spanner::v1::Type const&) { + if (pv.kind_case() != google::protobuf::Value::kStringValue) { + return Status(StatusCode::kUnknown, "missing JSONB"); + } + return JsonB(pv.string_value()); +} + StatusOr Value::GetValue(Numeric const&, google::protobuf::Value const& pv, google::spanner::v1::Type const&) { diff --git a/google/cloud/spanner/value.h b/google/cloud/spanner/value.h index bc5f4210ed1c..104705311d63 100644 --- a/google/cloud/spanner/value.h +++ b/google/cloud/spanner/value.h @@ -65,6 +65,7 @@ GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN * STRING | `std::string` * BYTES | `google::cloud::spanner::Bytes` * JSON | `google::cloud::spanner::Json` + * JSONB | `google::cloud::spanner::JsonB` * NUMERIC | `google::cloud::spanner::Numeric` * NUMERIC(PG) | `google::cloud::spanner::PgNumeric` * TIMESTAMP | `google::cloud::spanner::Timestamp` @@ -194,6 +195,8 @@ class Value { /// @copydoc Value(bool) explicit Value(Json v) : Value(PrivateConstructor{}, std::move(v)) {} /// @copydoc Value(bool) + explicit Value(JsonB v) : Value(PrivateConstructor{}, std::move(v)) {} + /// @copydoc Value(bool) explicit Value(Numeric v) : Value(PrivateConstructor{}, std::move(v)) {} /// @copydoc Value(bool) explicit Value(PgNumeric v) : Value(PrivateConstructor{}, std::move(v)) {} @@ -366,6 +369,7 @@ class Value { static bool TypeProtoIs(std::string const&, google::spanner::v1::Type const&); static bool TypeProtoIs(Bytes const&, google::spanner::v1::Type const&); static bool TypeProtoIs(Json const&, google::spanner::v1::Type const&); + static bool TypeProtoIs(JsonB const&, google::spanner::v1::Type const&); static bool TypeProtoIs(Numeric const&, google::spanner::v1::Type const&); static bool TypeProtoIs(PgNumeric const&, google::spanner::v1::Type const&); template @@ -414,6 +418,7 @@ class Value { static google::spanner::v1::Type MakeTypeProto(std::string const&); static google::spanner::v1::Type MakeTypeProto(Bytes const&); static google::spanner::v1::Type MakeTypeProto(Json const&); + static google::spanner::v1::Type MakeTypeProto(JsonB const&); static google::spanner::v1::Type MakeTypeProto(Numeric const&); static google::spanner::v1::Type MakeTypeProto(PgNumeric const&); static google::spanner::v1::Type MakeTypeProto(Timestamp); @@ -476,6 +481,7 @@ class Value { static google::protobuf::Value MakeValueProto(std::string s); static google::protobuf::Value MakeValueProto(Bytes b); static google::protobuf::Value MakeValueProto(Json j); + static google::protobuf::Value MakeValueProto(JsonB j); static google::protobuf::Value MakeValueProto(Numeric n); static google::protobuf::Value MakeValueProto(PgNumeric n); static google::protobuf::Value MakeValueProto(Timestamp ts); @@ -541,6 +547,8 @@ class Value { google::spanner::v1::Type const&); static StatusOr GetValue(Json const&, google::protobuf::Value const&, google::spanner::v1::Type const&); + static StatusOr GetValue(JsonB const&, google::protobuf::Value const&, + google::spanner::v1::Type const&); static StatusOr GetValue(Numeric const&, google::protobuf::Value const&, google::spanner::v1::Type const&); diff --git a/google/cloud/spanner/value_test.cc b/google/cloud/spanner/value_test.cc index 06d84b745e04..fbf368d73f1a 100644 --- a/google/cloud/spanner/value_test.cc +++ b/google/cloud/spanner/value_test.cc @@ -150,6 +150,16 @@ TEST(Value, BasicSemantics) { TestBasicSemantics(v); } + for (auto const& x : std::vector{JsonB(), JsonB(R"("Hello world!")"), + JsonB("42"), JsonB("true")}) { + SCOPED_TRACE("Testing: JSONB " + std::string(x)); + TestBasicSemantics(x); + TestBasicSemantics(std::vector(5, x)); + std::vector> v(5, x); + v.resize(10); + TestBasicSemantics(v); + } + for (auto const& x : { MakeNumeric(-0.9e29).value(), MakeNumeric(-1).value(), @@ -224,6 +234,7 @@ TEST(Value, Equality) { {Value("foo"), Value("bar")}, {Value(Bytes("foo")), Value(Bytes("bar"))}, {Value(Json("42")), Value(Json("true"))}, + {Value(JsonB("42")), Value(JsonB("true"))}, {Value(MakeNumeric(0).value()), Value(MakeNumeric(1).value())}, {Value(MakePgNumeric(0).value()), Value(MakePgNumeric(1).value())}, {Value(absl::CivilDay(1970, 1, 1)), Value(absl::CivilDay(2020, 3, 15))}, @@ -391,6 +402,14 @@ TEST(Value, JsonRelationalOperators) { EXPECT_NE(j1, j2); } +TEST(Value, JsonBRelationalOperators) { + JsonB jb1("42"); + JsonB jb2("true"); + + EXPECT_EQ(jb1, jb1); + EXPECT_NE(jb1, jb2); +} + TEST(Value, ConstructionFromLiterals) { Value v_int64(42); EXPECT_EQ(42, *v_int64.get()); @@ -693,6 +712,22 @@ TEST(Value, ProtoConversionJson) { auto const p = spanner_internal::ToProto(v); EXPECT_EQ(v, spanner_internal::FromProto(p.first, p.second)); EXPECT_EQ(google::spanner::v1::TypeCode::JSON, p.first.code()); + EXPECT_EQ(google::spanner::v1::TypeAnnotationCode:: + TYPE_ANNOTATION_CODE_UNSPECIFIED, + p.first.type_annotation()); + EXPECT_EQ(std::string(x), p.second.string_value()); + } +} + +TEST(Value, ProtoConversionJsonB) { + for (auto const& x : std::vector{JsonB(), JsonB(R"("Hello world!")"), + JsonB("42"), JsonB("true")}) { + Value const v(x); + auto const p = spanner_internal::ToProto(v); + EXPECT_EQ(v, spanner_internal::FromProto(p.first, p.second)); + EXPECT_EQ(google::spanner::v1::TypeCode::JSON, p.first.code()); + EXPECT_EQ(google::spanner::v1::TypeAnnotationCode::PG_JSONB, + p.first.type_annotation()); EXPECT_EQ(std::string(x), p.second.string_value()); } } @@ -910,6 +945,21 @@ TEST(Value, GetBadJson) { EXPECT_THAT(v.get(), Not(IsOk())); } +TEST(Value, GetBadJsonB) { + Value v(JsonB("true")); + ClearProtoKind(v); + EXPECT_THAT(v.get(), Not(IsOk())); + + SetProtoKind(v, google::protobuf::NULL_VALUE); + 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())); +} + TEST(Value, GetBadNumeric) { Value v(MakeNumeric(0).value()); ClearProtoKind(v); @@ -1120,6 +1170,8 @@ TEST(Value, OutputStream) { {Value(Bytes(std::string("DEADBEEF"))), R"(B"DEADBEEF")", normal}, {Value(Json()), "null", normal}, {Value(Json("true")), "true", normal}, + {Value(JsonB()), "null", normal}, + {Value(JsonB("true")), "true", normal}, {Value(MakeNumeric(1234567890).value()), "1234567890", normal}, {Value(MakePgNumeric(1234567890).value()), "1234567890", normal}, {Value(absl::CivilDay()), "1970-01-01", normal}, @@ -1145,6 +1197,7 @@ TEST(Value, OutputStream) { {MakeNullValue(), "NULL", normal}, {MakeNullValue(), "NULL", normal}, {MakeNullValue(), "NULL", normal}, + {MakeNullValue(), "NULL", normal}, {MakeNullValue(), "NULL", normal}, {MakeNullValue(), "NULL", normal}, {MakeNullValue(), "NULL", normal}, @@ -1159,6 +1212,7 @@ TEST(Value, OutputStream) { {Value(std::vector{"a", "b"}), R"(["a", "b"])", normal}, {Value(std::vector{2}), R"([B"", B""])", normal}, {Value(std::vector{2}), R"([null, null])", normal}, + {Value(std::vector{2}), R"([null, null])", normal}, {Value(std::vector{2}), "[0, 0]", normal}, {Value(std::vector{2}), "[1970-01-01, 1970-01-01]", normal}, @@ -1175,6 +1229,7 @@ TEST(Value, OutputStream) { {MakeNullValue>(), "NULL", normal}, {MakeNullValue>(), "NULL", normal}, {MakeNullValue>(), "NULL", normal}, + {MakeNullValue>(), "NULL", normal}, {MakeNullValue>(), "NULL", normal}, {MakeNullValue>(), "NULL", normal}, {MakeNullValue>(), "NULL", normal}, @@ -1219,6 +1274,7 @@ TEST(Value, OutputStream) { {MakeNullValue>(), "NULL", normal}, {MakeNullValue>(), "NULL", normal}, {MakeNullValue>>(), "NULL", normal}, + {MakeNullValue>>(), "NULL", normal}, }; for (auto const& tc : test_case) { @@ -1280,6 +1336,12 @@ TEST(Value, OutputStreamMatchesT) { StreamMatchesValueStream(Json("42")); StreamMatchesValueStream(Json("true")); + // JSONB + StreamMatchesValueStream(JsonB()); + StreamMatchesValueStream(JsonB(R"("Hello world!")")); + StreamMatchesValueStream(JsonB("42")); + StreamMatchesValueStream(JsonB("true")); + // Numeric StreamMatchesValueStream(MakeNumeric("999").value()); StreamMatchesValueStream(MakeNumeric(3.14159).value());