diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a11b5618bd..91e03c5a897 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ ### Fixed * ([#????](https://github.com/realm/realm-core/issues/????), since v?.?.?) +* Querying with object list arguments does not work. ([#6688](https://github.com/realm/realm-core/issues/6688), since v10.3.3) * Fix SessionWrapper use-after-free crash when tearing down sessions when using session multiplexing ([#6656](https://github.com/realm/realm-core/issues/6656), since v13.9.3) ### Breaking changes diff --git a/src/realm/parser/driver.cpp b/src/realm/parser/driver.cpp index 4b2915b022a..561707573be 100644 --- a/src/realm/parser/driver.cpp +++ b/src/realm/parser/driver.cpp @@ -435,37 +435,59 @@ Query EqualityNode::visit(ParserDriver* drv) auto left_type = left->get_type(); auto right_type = right->get_type(); - auto handle_typed_link = [drv](std::unique_ptr& list, std::unique_ptr& value, DataType& type) { + auto handle_typed_links = [drv](std::unique_ptr& list, std::unique_ptr& expr, DataType& type) { if (auto link_column = dynamic_cast*>(list.get())) { - if (value->get_mixed().is_null()) { - type = ColumnTypeTraits::id; - value = std::make_unique>(); - } - else { - auto left_dest_table_key = link_column->link_map().get_target_table()->get_key(); - auto right_table_key = value->get_mixed().get_link().get_table_key(); - auto right_obj_key = value->get_mixed().get_link().get_obj_key(); - if (left_dest_table_key == right_table_key) { - value = std::make_unique>(right_obj_key); - type = type_Link; - } - else { - const Group* g = drv->m_base_table->get_parent_group(); - throw InvalidQueryArgError(util::format( - "The relationship '%1' which links to type '%2' cannot be compared to an argument of type %3", - link_column->link_map().description(drv->m_serializer_state), - link_column->link_map().get_target_table()->get_class_name(), - print_pretty_objlink(value->get_mixed().get_link(), g))); + // Change all TypedLink values to ObjKey values + auto value = dynamic_cast(expr.get()); + auto left_dest_table_key = link_column->link_map().get_target_table()->get_key(); + auto sz = value->size(); + auto obj_keys = std::make_unique>(); + obj_keys->init(expr->has_multiple_values(), sz); + obj_keys->set_comparison_type(expr->get_comparison_type()); + for (size_t i = 0; i < sz; i++) { + auto val = value->get(i); + // i'th entry is already NULL + if (!val.is_null()) { + TableKey right_table_key; + ObjKey right_obj_key; + if (val.is_type(type_Link)) { + right_table_key = left_dest_table_key; + right_obj_key = val.get(); + } + else if (val.is_type(type_TypedLink)) { + right_table_key = val.get_link().get_table_key(); + right_obj_key = val.get_link().get_obj_key(); + } + else { + const char* target_type = get_data_type_name(val.get_type()); + throw InvalidQueryError( + util::format("Unsupported comparison between '%1' and type '%2'", + link_column->link_map().description(drv->m_serializer_state), target_type)); + } + if (left_dest_table_key == right_table_key) { + obj_keys->set(i, right_obj_key); + } + else { + const Group* g = drv->m_base_table->get_parent_group(); + throw InvalidQueryArgError( + util::format("The relationship '%1' which links to type '%2' cannot be compared to " + "an argument of type %3", + link_column->link_map().description(drv->m_serializer_state), + link_column->link_map().get_target_table()->get_class_name(), + print_pretty_objlink(ObjLink(right_table_key, right_obj_key), g))); + } } } + expr = std::move(obj_keys); + type = type_Link; } }; - if (left_type == type_Link && right_type == type_TypedLink && right->has_single_value()) { - handle_typed_link(left, right, right_type); + if (left_type == type_Link && right->has_constant_evaluation()) { + handle_typed_links(left, right, right_type); } - if (right_type == type_Link && left_type == type_TypedLink && left->has_single_value()) { - handle_typed_link(right, left, left_type); + if (right_type == type_Link && left->has_constant_evaluation()) { + handle_typed_links(right, left, left_type); } if (left_type.is_valid() && right_type.is_valid() && !Mixed::data_types_are_comparable(left_type, right_type)) { @@ -488,9 +510,33 @@ Query EqualityNode::visit(ParserDriver* drv) } } - const ObjPropertyBase* prop = dynamic_cast(left.get()); - if (right->has_single_value() && (left_type == right_type || left_type == type_Mixed)) { + if (left_type == type_Link && left_type == right_type && right->has_constant_evaluation()) { + if (auto link_column = dynamic_cast*>(left.get())) { + if (link_column->link_map().get_nb_hops() == 1 && + link_column->get_comparison_type().value_or(ExpressionComparisonType::Any) == + ExpressionComparisonType::Any) { + REALM_ASSERT(dynamic_cast*>(right.get())); + auto link_values = static_cast*>(right.get()); + // We can use a LinksToNode based query + std::vector values; + values.reserve(link_values->size()); + for (auto val : *link_values) { + values.emplace_back(val.is_null() ? ObjKey() : val.get()); + } + if (op == CompareNode::EQUAL) { + return drv->m_base_table->where().links_to(link_column->link_map().get_first_column_key(), + values); + } + else if (op == CompareNode::NOT_EQUAL) { + return drv->m_base_table->where().not_links_to(link_column->link_map().get_first_column_key(), + values); + } + } + } + } + else if (right->has_single_value() && (left_type == right_type || left_type == type_Mixed)) { Mixed val = right->get_mixed(); + const ObjPropertyBase* prop = dynamic_cast(left.get()); if (prop && !prop->links_exist()) { auto col_key = prop->column_key(); if (val.is_null()) { @@ -529,20 +575,6 @@ Query EqualityNode::visit(ParserDriver* drv) break; } } - else if (left_type == type_Link) { - auto link_column = dynamic_cast*>(left.get()); - if (link_column && link_column->link_map().get_nb_hops() == 1 && - link_column->get_comparison_type().value_or(ExpressionComparisonType::Any) == - ExpressionComparisonType::Any) { - // We can use equal/not_equal and get a LinksToNode based query - if (op == CompareNode::EQUAL) { - return drv->m_base_table->where().equal(link_column->link_map().get_first_column_key(), val); - } - else if (op == CompareNode::NOT_EQUAL) { - return drv->m_base_table->where().not_equal(link_column->link_map().get_first_column_key(), val); - } - } - } } if (case_sensitive) { switch (op) { @@ -1783,11 +1815,17 @@ std::unique_ptr LinkChain::column(const std::string& col) } } + auto col_type{col_key.get_type()}; + if (Table::is_link_type(col_type)) { + add(col_key); + return create_subexpr(col_key); + } + if (col_key.is_dictionary()) { return create_subexpr(col_key); } else if (col_key.is_set()) { - switch (col_key.get_type()) { + switch (col_type) { case col_type_Int: return create_subexpr>(col_key); case col_type_Bool: @@ -1810,15 +1848,12 @@ std::unique_ptr LinkChain::column(const std::string& col) return create_subexpr>(col_key); case col_type_Mixed: return create_subexpr>(col_key); - case col_type_Link: - add(col_key); - return create_subexpr(col_key); default: break; } } else if (col_key.is_list()) { - switch (col_key.get_type()) { + switch (col_type) { case col_type_Int: return create_subexpr>(col_key); case col_type_Bool: @@ -1841,9 +1876,6 @@ std::unique_ptr LinkChain::column(const std::string& col) return create_subexpr>(col_key); case col_type_Mixed: return create_subexpr>(col_key); - case col_type_LinkList: - add(col_key); - return create_subexpr(col_key); default: break; } @@ -1854,7 +1886,7 @@ std::unique_ptr LinkChain::column(const std::string& col) expression_cmp_type_to_str(m_comparison_type))); } - switch (col_key.get_type()) { + switch (col_type) { case col_type_Int: return create_subexpr(col_key); case col_type_Bool: @@ -1877,9 +1909,6 @@ std::unique_ptr LinkChain::column(const std::string& col) return create_subexpr(col_key); case col_type_Mixed: return create_subexpr(col_key); - case col_type_Link: - add(col_key); - return create_subexpr(col_key); default: break; } diff --git a/src/realm/query.cpp b/src/realm/query.cpp index 4222fc3fd26..9629ab0bebb 100644 --- a/src/realm/query.cpp +++ b/src/realm/query.cpp @@ -546,6 +546,12 @@ Query& Query::links_to(ColKey origin_column, const std::vector& target_k return *this; } +Query& Query::not_links_to(ColKey origin_column_key, const std::vector& target_keys) +{ + add_node(std::unique_ptr(new LinksToNode(origin_column_key, target_keys))); + return *this; +} + // int64 constant vs column Query& Query::equal(ColKey column_key, int64_t value) { diff --git a/src/realm/query.hpp b/src/realm/query.hpp index 9648daf8161..87b8e141840 100644 --- a/src/realm/query.hpp +++ b/src/realm/query.hpp @@ -105,6 +105,9 @@ class Query final { // Find links that point to specific target objects Query& links_to(ColKey column_key, const std::vector& target_obj); + // Find links that does not point to specific target objects + Query& not_links_to(ColKey column_key, const std::vector& target_obj); + // Conditions: null Query& equal(ColKey column_key, null); Query& not_equal(ColKey column_key, null); diff --git a/src/realm/query_engine.cpp b/src/realm/query_engine.cpp index 855709f4150..e24d243efa0 100644 --- a/src/realm/query_engine.cpp +++ b/src/realm/query_engine.cpp @@ -819,23 +819,43 @@ ExpressionNode::ExpressionNode(const ExpressionNode& from) template <> size_t LinksToNode::find_first_local(size_t start, size_t end) { - if (m_column_type == col_type_LinkList || m_condition_column_key.is_set()) { - // LinkLists never contain null - if (!m_target_keys[0] && m_target_keys.size() == 1 && start != end) - return not_found; - - BPlusTree links(m_table.unchecked_ptr()->get_alloc()); - for (size_t i = start; i < end; i++) { - if (ref_type ref = get_ref(i)) { - links.init_from_ref(ref); - for (auto& key : m_target_keys) { - if (key) { - if (links.find_first(key) != not_found) + if (m_condition_column_key.is_collection()) { + Allocator& alloc = m_table.unchecked_ptr()->get_alloc(); + if (m_condition_column_key.is_dictionary()) { + auto target_table_key = m_table->get_opposite_table(m_condition_column_key)->get_key(); + Array top(alloc); + for (size_t i = start; i < end; i++) { + if (ref_type ref = get_ref(i)) { + top.init_from_ref(ref); + BPlusTree values(alloc); + values.set_parent(&top, 1); + values.init_from_parent(); + for (auto& key : m_target_keys) { + ObjLink link(target_table_key, key); + if (values.find_first(link) != not_found) return i; } } } } + else { + // LinkLists never contain null + if (!m_target_keys[0] && m_target_keys.size() == 1 && start != end) + return not_found; + + BPlusTree links(alloc); + for (size_t i = start; i < end; i++) { + if (ref_type ref = get_ref(i)) { + links.init_from_ref(ref); + for (auto& key : m_target_keys) { + if (key) { + if (links.find_first(key) != not_found) + return i; + } + } + } + } + } } else if (m_list) { for (auto& key : m_target_keys) { @@ -856,15 +876,41 @@ size_t LinksToNode::find_first_local(size_t start, size_t end) REALM_ASSERT(m_target_keys.size() == 1); ObjKey key = m_target_keys[0]; - if (m_column_type == col_type_LinkList || m_condition_column_key.is_set()) { - BPlusTree links(m_table.unchecked_ptr()->get_alloc()); - for (size_t i = start; i < end; i++) { - if (ref_type ref = get_ref(i)) { - links.init_from_ref(ref); - auto sz = links.size(); - for (size_t j = 0; j < sz; j++) { - if (links.get(j) != key) { + if (m_condition_column_key.is_collection()) { + Allocator& alloc = m_table.unchecked_ptr()->get_alloc(); + if (m_condition_column_key.is_dictionary()) { + auto target_table_key = m_table->get_opposite_table(m_condition_column_key)->get_key(); + Array top(alloc); + for (size_t i = start; i < end; i++) { + if (ref_type ref = get_ref(i)) { + top.init_from_ref(ref); + BPlusTree values(alloc); + values.set_parent(&top, 1); + values.init_from_parent(); + + ObjLink link(target_table_key, key); + bool found = false; + values.for_all([&](const Mixed& val) { + if (val != link) { + found = true; + } + return !found; + }); + if (found) return i; + } + } + } + else { + BPlusTree links(alloc); + for (size_t i = start; i < end; i++) { + if (ref_type ref = get_ref(i)) { + links.init_from_ref(ref); + auto sz = links.size(); + for (size_t j = 0; j < sz; j++) { + if (links.get(j) != key) { + return i; + } } } } diff --git a/src/realm/query_engine.hpp b/src/realm/query_engine.hpp index af97159c359..33e0dcb75ef 100644 --- a/src/realm/query_engine.hpp +++ b/src/realm/query_engine.hpp @@ -2351,37 +2351,46 @@ class LinksToNodeBase : public ParentNode { { m_dT = 50.0; m_condition_column_key = origin_column_key; - m_column_type = origin_column_key.get_type(); - REALM_ASSERT(m_column_type == col_type_Link || m_column_type == col_type_LinkList); + auto column_type = origin_column_key.get_type(); + REALM_ASSERT(column_type == col_type_Link || column_type == col_type_LinkList); REALM_ASSERT(!m_target_keys.empty()); } void cluster_changed() override { - if (m_column_type == col_type_Link) { - m_list.emplace(m_table.unchecked_ptr()->get_alloc()); - m_leaf = &*m_list; - } - else if (m_column_type == col_type_LinkList) { + if (m_condition_column_key.is_collection()) { m_linklist.emplace(m_table.unchecked_ptr()->get_alloc()); m_leaf = &*m_linklist; } + else { + m_list.emplace(m_table.unchecked_ptr()->get_alloc()); + m_leaf = &*m_list; + } m_cluster->init_leaf(this->m_condition_column_key, m_leaf); } std::string describe(util::serializer::SerialisationState& state) const override { REALM_ASSERT(m_condition_column_key); - if (m_target_keys.size() > 1) - throw SerializationError("Serializing a query which links to multiple objects is currently unsupported."); - ObjLink link(m_table->get_opposite_table(m_condition_column_key)->get_key(), m_target_keys[0]); + std::string links = m_target_keys.size() > 1 ? "{" : ""; + Group* g = m_table->get_parent_group(); + auto target_table_key = m_table->get_opposite_table(m_condition_column_key)->get_key(); + int cnt = 0; + for (auto key : m_target_keys) { + if (cnt++) { + links += ","; + } + links += util::serializer::print_value(ObjLink(target_table_key, key), g); + } + if (m_target_keys.size() > 1) { + links += "}"; + } return state.describe_column(ParentNode::m_table, m_condition_column_key) + " " + describe_condition() + " " + - util::serializer::print_value(link, m_table->get_parent_group()); + links; } protected: std::vector m_target_keys; - ColumnType m_column_type; std::optional m_list; std::optional m_linklist; ArrayPayload* m_leaf = nullptr; @@ -2389,7 +2398,6 @@ class LinksToNodeBase : public ParentNode { LinksToNodeBase(const LinksToNodeBase& source) : ParentNode(source) , m_target_keys(source.m_target_keys) - , m_column_type(source.m_column_type) { } diff --git a/test/object-store/c_api/c_api.cpp b/test/object-store/c_api/c_api.cpp index 56eb3cbd5d2..e377928c0b6 100644 --- a/test/object-store/c_api/c_api.cpp +++ b/test/object-store/c_api/c_api.cpp @@ -2408,6 +2408,37 @@ TEST_CASE("C API", "[c_api]") { CHECK(1 == count_list); } + SECTION("link in list") { + auto link = rlm_link_val(class_bar.key, realm_object_get_key(obj2.get())); + realm_value_t link_value = link; + write([&]() { + CHECK(realm_set_value(obj1.get(), foo_properties["link"], link_value, false)); + }); + + static const size_t num_args = 1; + realm_query_arg_t args[num_args] = {realm_query_arg_t{1, false, &link_value}}; + realm_query_arg_t* arg = &args[0]; + + realm_value_t list_arg[num_args] = {link_value}; + realm_query_arg_t args_in_list[num_args] = {realm_query_arg_t{num_args, true, &list_arg[0]}}; + realm_query_arg_t* arg_list = &args_in_list[0]; + + auto q_link_single_param = + cptr_checked(realm_query_parse(realm, class_foo.key, "link == $0", num_args, arg)); + auto q_link_in_list = + cptr_checked(realm_query_parse(realm, class_foo.key, "link IN $0", num_args, arg_list)); + + size_t count, count_list; + + // change the link + link = rlm_null(); + + CHECK(checked(realm_query_count(q_link_single_param.get(), &count))); + CHECK(1 == count); + CHECK(checked(realm_query_count(q_link_in_list.get(), &count_list))); + CHECK(1 == count_list); + } + SECTION("decimal NaN") { realm_value_t decimal = rlm_decimal_nan(); diff --git a/test/test_parser.cpp b/test/test_parser.cpp index f6e8b149940..608349273ab 100644 --- a/test/test_parser.cpp +++ b/test/test_parser.cpp @@ -5040,6 +5040,7 @@ TEST(Parser_DictionaryObjects) Obj adam = persons->create_object_with_primary_key("adam"); Obj bernie = persons->create_object_with_primary_key("bernie"); + Obj charlie = persons->create_object_with_primary_key("charlie"); Obj astro = dogs->create_object_with_primary_key("astro", {{col_age, 4}}); Obj pluto = dogs->create_object_with_primary_key("pluto", {{col_age, 5}}); @@ -5055,17 +5056,22 @@ TEST(Parser_DictionaryObjects) bernie_pets.insert("dog1", astro); bernie_pets.insert("dog2", snoopy); + auto charlie_pets = charlie.get_dictionary(col_dict); + charlie_pets.insert("dog1", pluto); + adam.set(col_friend, bernie.get_key()); bernie.set(col_friend, adam.get_key()); auto q = persons->link(col_dict).column(col_age) > 4; - CHECK_EQUAL(q.count(), 1); + CHECK_EQUAL(q.count(), 2); q = persons->link(col_friend).link(col_dict).column(col_age) > 4; CHECK_EQUAL(q.count(), 1); - verify_query(test_context, persons, "pets.@values.age > 4", 1); - verify_query(test_context, persons, "pets.@values == obj('dog', 'pluto')", 1); - verify_query(test_context, persons, "pets.@values == ANY { obj('dog', 'pluto'), obj('dog', 'astro') }", 2); + verify_query(test_context, persons, "pets.@values.age > 4", 2); + verify_query(test_context, persons, "pets.@values == obj('dog', 'pluto')", 2); + verify_query(test_context, persons, "pets.@values != obj('dog', 'pluto')", 2); + verify_query(test_context, persons, "pets.@values == ANY { obj('dog', 'lady'), obj('dog', 'astro') }", 2); + verify_query(test_context, persons, "pets.@values == ANY { obj('dog', 'astro'), NULL }", 2); } TEST(Parser_DictionarySorting)