From aed3feee2c4f2ad411596bc19155fe0508c7dd9f Mon Sep 17 00:00:00 2001 From: Ben Pope Date: Thu, 1 Aug 2024 12:20:34 +0100 Subject: [PATCH 1/4] schema_registry/json: Encapsulate refs in the impl Signed-off-by: Ben Pope --- src/v/pandaproxy/schema_registry/json.cc | 18 ++++++++++++++---- src/v/pandaproxy/schema_registry/types.h | 11 +++-------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/v/pandaproxy/schema_registry/json.cc b/src/v/pandaproxy/schema_registry/json.cc index 6afb8fe9502f..d74cb4f83884 100644 --- a/src/v/pandaproxy/schema_registry/json.cc +++ b/src/v/pandaproxy/schema_registry/json.cc @@ -63,12 +63,17 @@ struct json_schema_definition::impl { return std::move(buf).as_iobuf(); } - explicit impl(json::Document doc, std::string_view name) + explicit impl( + json::Document doc, + std::string_view name, + canonical_schema_definition::references refs) : doc{std::move(doc)} - , name{name} {} + , name{name} + , refs(std::move(refs)) {} json::Document doc; ss::sstring name; + canonical_schema_definition::references refs; }; bool operator==( @@ -89,6 +94,11 @@ canonical_schema_definition::raw_string json_schema_definition::raw() const { return canonical_schema_definition::raw_string{_impl->to_json()}; } +canonical_schema_definition::references const& +json_schema_definition::refs() const { + return _impl->refs; +} + ss::sstring json_schema_definition::name() const { return {_impl->name}; }; namespace { @@ -1509,8 +1519,8 @@ make_json_schema_definition(sharded_store&, canonical_schema schema) { std::string_view name = schema.sub()(); auto refs = std::move(schema).def().refs(); co_return json_schema_definition{ - ss::make_shared(std::move(doc), name), - std::move(refs)}; + ss::make_shared( + std::move(doc), name, std::move(refs))}; } ss::future make_canonical_json_schema( diff --git a/src/v/pandaproxy/schema_registry/types.h b/src/v/pandaproxy/schema_registry/types.h index 42426f29bdf7..52e39817e841 100644 --- a/src/v/pandaproxy/schema_registry/types.h +++ b/src/v/pandaproxy/schema_registry/types.h @@ -280,15 +280,11 @@ class json_schema_definition { struct impl; using pimpl = ss::shared_ptr; - explicit json_schema_definition( - pimpl p, canonical_schema_definition::references refs) - : _impl{std::move(p)} - , _refs(std::move(refs)) {} + explicit json_schema_definition(pimpl p) + : _impl{std::move(p)} {} canonical_schema_definition::raw_string raw() const; - canonical_schema_definition::references const& refs() const { - return _refs; - }; + canonical_schema_definition::references const& refs() const; const impl& operator()() const { return *_impl; } @@ -308,7 +304,6 @@ class json_schema_definition { private: pimpl _impl; - canonical_schema_definition::references _refs; }; ///\brief A schema that has been validated. From d43f488b3c11edf7dc951afd3904a88509f4a0cf Mon Sep 17 00:00:00 2001 From: Ben Pope Date: Fri, 2 Aug 2024 12:17:40 +0100 Subject: [PATCH 2/4] schema_registry/json: Introduce as_string_view Signed-off-by: Ben Pope --- src/v/pandaproxy/schema_registry/json.cc | 27 ++++++++++-------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/src/v/pandaproxy/schema_registry/json.cc b/src/v/pandaproxy/schema_registry/json.cc index d74cb4f83884..fb3a2200a690 100644 --- a/src/v/pandaproxy/schema_registry/json.cc +++ b/src/v/pandaproxy/schema_registry/json.cc @@ -103,6 +103,10 @@ ss::sstring json_schema_definition::name() const { return {_impl->name}; }; namespace { +std::string_view as_string_view(json::Value const& v) { + return {v.GetString(), v.GetStringLength()}; +} + ss::future<> check_references(sharded_store& store, canonical_schema schema) { for (const auto& ref : schema.def().refs()) { co_await store.is_subject_version_deleted(ref.sub, ref.version) @@ -289,7 +293,7 @@ result parse_json(iobuf buf) { // schema is an actual object if (auto it = schema.FindMember("$schema"); it != schema.MemberEnd()) { if (it->value.IsString()) { - dialect = from_uri(it->value.GetString()); + dialect = from_uri(as_string_view(it->value)); } if (it->value.IsString() == false || dialect == std::nullopt) { @@ -373,7 +377,7 @@ constexpr std::optional from_string_view(std::string_view v) { } constexpr auto parse_json_type(json::Value const& v) { - std::string_view sv{v.GetString(), v.GetStringLength()}; + auto sv = as_string_view(v); auto type = from_string_view(sv); if (!type) { throw as_exception(error_info{ @@ -717,11 +721,7 @@ bool is_string_superset(json::Value const& older, json::Value const& newer) { // both have "pattern". check if they are the same, the only // possible_value_accepted - auto older_pattern = std::string_view{ - older_val_p->GetString(), older_val_p->GetStringLength()}; - auto newer_pattern = std::string_view{ - newer_val_p->GetString(), newer_val_p->GetStringLength()}; - return older_pattern == newer_pattern; + return as_string_view(*older_val_p) == as_string_view(*newer_val_p); } bool is_numeric_superset(json::Value const& older, json::Value const& newer) { @@ -999,13 +999,11 @@ bool is_object_properties_superset( // or it should be checked against every schema in // older["patternProperties"] that matches auto pattern_match_found = false; - for (auto pname - = std::string_view{prop.GetString(), prop.GetStringLength()}; + for (auto pname = as_string_view(prop); auto const& [propPattern, schemaPattern] : older_pattern_properties) { // TODO this rebuilds the regex each time, could be cached - auto regex = re2::RE2(std::string_view{ - propPattern.GetString(), propPattern.GetStringLength()}); + auto regex = re2::RE2(as_string_view(propPattern)); if (re2::RE2::PartialMatch(pname, regex)) { pattern_match_found = true; if (!is_superset(schemaPattern, schema)) { @@ -1461,7 +1459,7 @@ bool check_compatible_dialects( if (it == v.MemberEnd()) { return std::nullopt; } - return from_uri(it->value.GetString()); + return from_uri(as_string_view(it->value)); }; auto older_dialect = get_dialect(older); @@ -1501,10 +1499,7 @@ void sort(json::Value& val) { case rapidjson::Type::kObjectType: { auto v = val.GetObject(); std::sort(v.begin(), v.end(), [](auto& lhs, auto& rhs) { - return std::string_view{ - lhs.name.GetString(), lhs.name.GetStringLength()} - < std::string_view{ - rhs.name.GetString(), rhs.name.GetStringLength()}; + return as_string_view(lhs.name) < as_string_view(rhs.name); }); } } From 87393e6754767aac3eb4b38d03752263aa87c498 Mon Sep 17 00:00:00 2001 From: Ben Pope Date: Fri, 2 Aug 2024 12:28:53 +0100 Subject: [PATCH 3/4] schema_registry/json: Introduce a context Signed-off-by: Ben Pope --- src/v/pandaproxy/schema_registry/json.cc | 129 ++++++++++++++--------- 1 file changed, 79 insertions(+), 50 deletions(-) diff --git a/src/v/pandaproxy/schema_registry/json.cc b/src/v/pandaproxy/schema_registry/json.cc index fb3a2200a690..362a8edf6971 100644 --- a/src/v/pandaproxy/schema_registry/json.cc +++ b/src/v/pandaproxy/schema_registry/json.cc @@ -177,6 +177,20 @@ struct pj { } }; +class schema_context { +public: + explicit schema_context(json_schema_definition::impl const& schema) + : _schema{schema} {} + +private: + json_schema_definition::impl const& _schema; +}; + +struct context { + schema_context older; + schema_context newer; +}; + template jsoncons::jsonschema::json_schema const& get_metaschema() { static auto const meteschema_doc = [] { @@ -328,7 +342,8 @@ result parse_json(iobuf buf) { // a schema O is a superset of another schema N if every schema that is valid // for N is also valid for O. precondition: older and newer are both valid // schemas -bool is_superset(json::Value const& older, json::Value const& newer); +bool is_superset( + context const& ctx, json::Value const& older, json::Value const& newer); // close the implementation in a namespace to keep it contained namespace is_superset_impl { @@ -485,7 +500,8 @@ json_type_list normalized_type(json::Value const& v) { } // helper to convert a boolean to a schema -json::Value::ConstObject get_schema(json::Value const& v) { +json::Value::ConstObject +get_schema(schema_context const&, json::Value const& v) { if (v.IsObject()) { return v.GetObject(); } @@ -503,12 +519,12 @@ json::Value::ConstObject get_schema(json::Value const& v) { // helper to retrieve the object value for a key, or an empty object if the key // is not present -json::Value::ConstObject -get_object_or_empty(json::Value const& v, std::string_view key) { +json::Value::ConstObject get_object_or_empty( + schema_context const& ctx, json::Value const& v, std::string_view key) { auto it = v.FindMember( json::Value{key.data(), rapidjson::SizeType(key.size())}); if (it != v.MemberEnd()) { - return get_schema(it->value); + return get_schema(ctx, it->value); } return get_true_schema(); @@ -629,6 +645,7 @@ bool is_numeric_property_value_superset( enum class additional_field_for { object, array }; bool is_additional_superset( + context const& ctx, json::Value const& older, json::Value const& newer, additional_field_for field_type) { @@ -676,25 +693,25 @@ bool is_additional_superset( // older=false -> newer=true - not compatible return older; }, - [](bool older, json::Value const* newer) { + [&ctx](bool older, json::Value const* newer) { if (older) { // true is compatible with any schema return true; } // likely false, but need to check - return is_superset(get_false_schema(), *newer); + return is_superset(ctx, get_false_schema(), *newer); }, - [](json::Value const* older, bool newer) { + [&ctx](json::Value const* older, bool newer) { if (!newer) { // any schema is compatible with false return true; } // convert newer to {} and check against that - return is_superset(*older, get_true_schema()); + return is_superset(ctx, *older, get_true_schema()); }, - [](json::Value const* older, json::Value const* newer) { + [&ctx](json::Value const* older, json::Value const* newer) { // check subschemas for compatibility - return is_superset(*older, *newer); + return is_superset(ctx, *older, *newer); }), get_additional_props(older), get_additional_props(newer)); @@ -839,7 +856,8 @@ bool is_numeric_superset(json::Value const& older, json::Value const& newer) { return true; } -bool is_array_superset(json::Value const& older, json::Value const& newer) { +bool is_array_superset( + context const& ctx, json::Value const& older, json::Value const& newer) { // "type": "array" is used to model an array or a tuple. // for array, "items" is a schema that validates all the elements. // for tuple in Draft4, "items" is an array of schemas to validate the @@ -913,8 +931,9 @@ bool is_array_superset(json::Value const& older, json::Value const& newer) { // not used by validation because every element is validated against // "items" return is_superset( - get_object_or_empty(older, "items"), - get_object_or_empty(newer, "items")); + ctx, + get_object_or_empty(ctx.older, older, "items"), + get_object_or_empty(ctx.newer, newer, "items")); } // both are tuple schemas, validation is similar to object. one side @@ -922,7 +941,8 @@ bool is_array_superset(json::Value const& older, json::Value const& newer) { // first check is for "additionalItems" compatibility, it's cheaper than the // rest - if (!is_additional_superset(older, newer, additional_field_for::array)) { + if (!is_additional_superset( + ctx, older, newer, additional_field_for::array)) { return false; } @@ -930,7 +950,11 @@ bool is_array_superset(json::Value const& older, json::Value const& newer) { auto newer_tuple_schema = newer["items"].GetArray(); // find the first pair of schemas that do not match auto [older_it, newer_it] = std::ranges::mismatch( - older_tuple_schema, newer_tuple_schema, is_superset); + older_tuple_schema, + newer_tuple_schema, + [&ctx](auto const& older, auto const& newer) { + return is_superset(ctx, older, newer); + }); if ( older_it != older_tuple_schema.end() @@ -948,18 +972,18 @@ bool is_array_superset(json::Value const& older, json::Value const& newer) { // excess elements with older["additionalItems"] auto older_additional_schema = get_object_or_empty( - older, "additionalItems"); + ctx.older, older, "additionalItems"); // check that all excess schemas are compatible with // older["additionalItems"] return std::all_of( newer_it, newer_tuple_schema.end(), [&](json::Value const& n) { - return is_superset(older_additional_schema, n); + return is_superset(ctx, older_additional_schema, n); }); } bool is_object_properties_superset( - json::Value const& older, json::Value const& newer) { + context const& ctx, json::Value const& older, json::Value const& newer) { // check that every property in newer["properties"] // if it appears in older["properties"], // then it has to be compatible with the schema @@ -968,27 +992,27 @@ bool is_object_properties_superset( // or // it has to be compatible with older["additionalProperties"] - auto newer_properties = get_object_or_empty(newer, "properties"); + auto newer_properties = get_object_or_empty(ctx.newer, newer, "properties"); if (newer_properties.ObjectEmpty()) { // no "properties" in newer, all good return true; } // older["properties"] is a map of - auto older_properties = get_object_or_empty(older, "properties"); + auto older_properties = get_object_or_empty(ctx.older, older, "properties"); // older["patternProperties"] is a map of auto older_pattern_properties = get_object_or_empty( - older, "patternProperties"); + ctx.older, older, "patternProperties"); // older["additionalProperties"] is a schema auto older_additional_properties = get_object_or_empty( - older, "additionalProperties"); + ctx.older, older, "additionalProperties"); // scan every prop in newer["properties"] for (auto const& [prop, schema] : newer_properties) { // it is either an evolution of a schema in older["properties"] if (auto older_it = older_properties.FindMember(prop); older_it != older_properties.MemberEnd()) { // prop exists in both - if (!is_superset(older_it->value, schema)) { + if (!is_superset(ctx, older_it->value, schema)) { // not compatible return false; } @@ -1006,7 +1030,7 @@ bool is_object_properties_superset( auto regex = re2::RE2(as_string_view(propPattern)); if (re2::RE2::PartialMatch(pname, regex)) { pattern_match_found = true; - if (!is_superset(schemaPattern, schema)) { + if (!is_superset(ctx, schemaPattern, schema)) { // not compatible return false; } @@ -1017,7 +1041,7 @@ bool is_object_properties_superset( // in patternProperties was found if ( !pattern_match_found - && !is_superset(older_additional_properties, schema)) { + && !is_superset(ctx, older_additional_properties, schema)) { // not compatible return false; } @@ -1027,15 +1051,15 @@ bool is_object_properties_superset( } bool is_object_pattern_properties_superset( - json::Value const& older, json::Value const& newer) { + context const& ctx, json::Value const& older, json::Value const& newer) { // check that every pattern property in newer["patternProperties"] // appears in older["patternProperties"] and is compatible with the schema // "patternProperties" is a map of auto newer_pattern_properties = get_object_or_empty( - newer, "patternProperties"); + ctx.newer, newer, "patternProperties"); auto older_pattern_properties = get_object_or_empty( - older, "patternProperties"); + ctx.older, older, "patternProperties"); // TODO O(n^2) lookup for (auto const& [pattern, schema] : newer_pattern_properties) { @@ -1046,7 +1070,7 @@ bool is_object_pattern_properties_superset( return false; } - if (!is_superset(older_pp_it->value, schema)) { + if (!is_superset(ctx, older_pp_it->value, schema)) { // not compatible return false; } @@ -1056,7 +1080,7 @@ bool is_object_pattern_properties_superset( } bool is_object_required_superset( - json::Value const& older, json::Value const& newer) { + context const& ctx, json::Value const& older, json::Value const& newer) { // to pass the check, a required property from newer has to be present in // older, or if new it needs to be without a default value note that: // 1. we check only required properties that are in both newer["properties"] @@ -1067,8 +1091,8 @@ bool is_object_required_superset( auto older_req = get_array_or_empty(older, "required"); auto newer_req = get_array_or_empty(newer, "required"); - auto older_props = get_object_or_empty(older, "properties"); - auto newer_props = get_object_or_empty(newer, "properties"); + auto older_props = get_object_or_empty(ctx.older, older, "properties"); + auto newer_props = get_object_or_empty(ctx.newer, newer, "properties"); // TODO O(n^2) lookup that can be a set_intersection auto newer_props_in_older = older_props @@ -1104,7 +1128,8 @@ bool is_object_required_superset( return true; } -bool is_object_superset(json::Value const& older, json::Value const& newer) { +bool is_object_superset( + context const& ctx, json::Value const& older, json::Value const& newer) { if (!is_numeric_property_value_superset( older, newer, "minProperties", std::less_equal<>{}, 0)) { // newer requires less properties to be set @@ -1115,11 +1140,12 @@ bool is_object_superset(json::Value const& older, json::Value const& newer) { // newer requires more properties to be set return false; } - if (!is_additional_superset(older, newer, additional_field_for::object)) { + if (!is_additional_superset( + ctx, older, newer, additional_field_for::object)) { // additional properties are not compatible return false; } - if (!is_object_properties_superset(older, newer)) { + if (!is_object_properties_superset(ctx, older, newer)) { // "properties" in newer might not be compatible with // older["properties"] (incompatible evolution) or // older["patternProperties"] (it is not compatible with the pattern @@ -1128,11 +1154,11 @@ bool is_object_superset(json::Value const& older, json::Value const& newer) { // newer) return false; } - if (!is_object_pattern_properties_superset(older, newer)) { + if (!is_object_pattern_properties_superset(ctx, older, newer)) { // pattern properties checks are not compatible return false; } - if (!is_object_required_superset(older, newer)) { + if (!is_object_required_superset(ctx, older, newer)) { // required properties are not compatible return false; } @@ -1182,7 +1208,7 @@ bool is_enum_superset(json::Value const& older, json::Value const& newer) { } bool is_not_combinator_superset( - json::Value const& older, json::Value const& newer) { + context const& ctx, json::Value const& older, json::Value const& newer) { auto older_it = older.FindMember("not"); auto newer_it = newer.FindMember("not"); auto older_has_not = older_it != older.MemberEnd(); @@ -1197,7 +1223,7 @@ bool is_not_combinator_superset( // for not combinator, we want to check if the "not" newer subschema is // less strict than the older subschema, because this means that newer // validated less data than older - return is_superset(newer_it->value, older_it->value); + return is_superset(ctx, newer_it->value, older_it->value); } // both do not have a "not" key, compatible @@ -1217,7 +1243,7 @@ json::Value to_keyword(p_combinator c) { } bool is_positive_combinator_superset( - json::Value const& older, json::Value const& newer) { + context const& ctx, json::Value const& older, json::Value const& newer) { auto get_combinator = [](json::Value const& v) { auto res = std::optional{}; for (auto c : @@ -1301,7 +1327,7 @@ bool is_positive_combinator_superset( auto superset_graph = graph_t{older_schemas.Size() + newer_schemas.Size()}; for (auto o = 0u; o < older_schemas.Size(); ++o) { for (auto n = 0u; n < newer_schemas.Size(); ++n) { - if (is_superset(older_schemas[o], newer_schemas[n])) { + if (is_superset(ctx, older_schemas[o], newer_schemas[n])) { // translate n for the graph auto n_index = n + older_schemas.Size(); add_edge(o, n_index, superset_graph); @@ -1335,7 +1361,9 @@ using namespace is_superset_impl; // for N is also valid for O. precondition: older and newer are both valid // schemas bool is_superset( - json::Value const& older_schema, json::Value const& newer_schema) { + context const& ctx, + json::Value const& older_schema, + json::Value const& newer_schema) { // break recursion if parameters are atoms: if (is_true_schema(older_schema) || is_false_schema(newer_schema)) { // either older is the superset of every possible schema, or newer is @@ -1343,8 +1371,8 @@ bool is_superset( return true; } - auto older = get_schema(older_schema); - auto newer = get_schema(newer_schema); + auto older = get_schema(ctx.older, older_schema); + auto newer = get_schema(ctx.newer, newer_schema); // extract { "type" : ... } auto older_types = normalized_type(older); @@ -1385,12 +1413,12 @@ bool is_superset( } break; case json_type::object: - if (!is_object_superset(older, newer)) { + if (!is_object_superset(ctx, older, newer)) { return false; } break; case json_type::array: - if (!is_array_superset(older, newer)) { + if (!is_array_superset(ctx, older, newer)) { return false; } break; @@ -1407,11 +1435,11 @@ bool is_superset( return false; } - if (!is_not_combinator_superset(older, newer)) { + if (!is_not_combinator_superset(ctx, older, newer)) { return false; } - if (!is_positive_combinator_superset(older, newer)) { + if (!is_positive_combinator_superset(ctx, older, newer)) { return false; } @@ -1554,7 +1582,8 @@ bool check_compatible( } // reader is a superset of writer iff every schema that is valid for writer // is also valid for reader - return is_superset(reader().doc, writer().doc); + context ctx{.older{reader()}, .newer{writer()}}; + return is_superset(ctx, reader().doc, writer().doc); } } // namespace pandaproxy::schema_registry From 955f0622c882f0287e9d8436692116060f73f419 Mon Sep 17 00:00:00 2001 From: Ben Pope Date: Fri, 2 Aug 2024 12:39:59 +0100 Subject: [PATCH 4/4] schema_registry/json: Support internal references Signed-off-by: Ben Pope --- src/v/pandaproxy/schema_registry/json.cc | 31 ++++++++++++++++--- .../schema_registry/test/test_json_schema.cc | 25 +++++++++++++++ 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/src/v/pandaproxy/schema_registry/json.cc b/src/v/pandaproxy/schema_registry/json.cc index 362a8edf6971..ac39abcf19ff 100644 --- a/src/v/pandaproxy/schema_registry/json.cc +++ b/src/v/pandaproxy/schema_registry/json.cc @@ -15,6 +15,7 @@ #include "json/chunked_input_stream.h" #include "json/document.h" #include "json/ostreamwrapper.h" +#include "json/pointer.h" #include "json/schema.h" #include "json/stringbuffer.h" #include "json/writer.h" @@ -182,6 +183,26 @@ class schema_context { explicit schema_context(json_schema_definition::impl const& schema) : _schema{schema} {} + json::Document::ValueType const& resolve(std::string_view ref) const { + // Internal reference + if (ref.starts_with("#")) { + json::Pointer ptr{ + ref.data() + 1, + ref.length() - 1, + }; + if (auto* p = ptr.Get(_schema.doc); p) { + return *p; + } + throw as_exception(error_info{ + error_code::schema_invalid, + fmt::format("Reference not found: '{}'", ref)}); + } + + throw as_exception(error_info{ + error_code::schema_invalid, + fmt::format("External references not supported: '{}'", ref)}); + } + private: json_schema_definition::impl const& _schema; }; @@ -499,10 +520,13 @@ json_type_list normalized_type(json::Value const& v) { return ret; } -// helper to convert a boolean to a schema +// helper to convert a boolean to a schema, and traverse references json::Value::ConstObject -get_schema(schema_context const&, json::Value const& v) { +get_schema(schema_context const& ctx, json::Value const& v) { if (v.IsObject()) { + if (auto it = v.FindMember("$ref"); it != v.MemberEnd()) { + return ctx.resolve(as_string_view(it->value)).GetObject(); + } return v.GetObject(); } @@ -1444,10 +1468,7 @@ bool is_superset( } for (auto not_yet_handled_keyword : { - "definitions", "dependencies", - // draft 6 unhandled keywords: - "$ref", // draft 2019-09 unhandled keywords: "dependentRequired", "dependentSchemas", diff --git a/src/v/pandaproxy/schema_registry/test/test_json_schema.cc b/src/v/pandaproxy/schema_registry/test/test_json_schema.cc index d53281336158..91c3477e2007 100644 --- a/src/v/pandaproxy/schema_registry/test/test_json_schema.cc +++ b/src/v/pandaproxy/schema_registry/test/test_json_schema.cc @@ -830,6 +830,31 @@ static constexpr auto compatibility_test_cases = std::to_array< = R"({"$schema": "http://json-schema.org/draft-06/schema#"})", .reader_is_compatible_with_writer = true, }, + // refs + { + .reader_schema = R"({"type": "string"})", + .writer_schema = R"({ + "$ref": "#/definitions/a_ref", + "definitions": { + "a_ref": { + "type": "string" + } + } +})", + .reader_is_compatible_with_writer = true, + }, + { + .reader_schema = R"({ + "$ref": "#/$defs/a_ref", + "$defs": { + "a_ref": { + "type": "string" + } + } +})", + .writer_schema = R"({"type": "string"})", + .reader_is_compatible_with_writer = true, + }, }); SEASTAR_THREAD_TEST_CASE(test_compatibility_check) { store_fixture f;