--- /dev/null
+////////////////////////////////////////////////////////////////////////////
+//
+// Copyright 2014 Realm Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////
+
+#import "RLMQueryUtil.hpp"
+
+#import "RLMArray.h"
+#import "RLMObjectSchema_Private.h"
+#import "RLMObject_Private.hpp"
+#import "RLMPredicateUtil.hpp"
+#import "RLMProperty_Private.h"
+#import "RLMSchema.h"
+#import "RLMUtil.hpp"
+
+#import "object_store.hpp"
+#import "results.hpp"
+
+#include <realm/query_engine.hpp>
+#include <realm/query_expression.hpp>
+#include <realm/util/cf_ptr.hpp>
+#include <realm/util/overload.hpp>
+
+using namespace realm;
+
+NSString * const RLMPropertiesComparisonTypeMismatchException = @"RLMPropertiesComparisonTypeMismatchException";
+NSString * const RLMUnsupportedTypesFoundInPropertyComparisonException = @"RLMUnsupportedTypesFoundInPropertyComparisonException";
+
+NSString * const RLMPropertiesComparisonTypeMismatchReason = @"Property type mismatch between %@ and %@";
+NSString * const RLMUnsupportedTypesFoundInPropertyComparisonReason = @"Comparison between %@ and %@";
+
+// small helper to create the many exceptions thrown when parsing predicates
+static NSException *RLMPredicateException(NSString *name, NSString *format, ...) {
+ va_list args;
+ va_start(args, format);
+ NSString *reason = [[NSString alloc] initWithFormat:format arguments:args];
+ va_end(args);
+
+ return [NSException exceptionWithName:name reason:reason userInfo:nil];
+}
+
+// check a precondition and throw an exception if it is not met
+// this should be used iff the condition being false indicates a bug in the caller
+// of the function checking its preconditions
+static void RLMPrecondition(bool condition, NSString *name, NSString *format, ...) {
+ if (__builtin_expect(condition, 1)) {
+ return;
+ }
+
+ va_list args;
+ va_start(args, format);
+ NSString *reason = [[NSString alloc] initWithFormat:format arguments:args];
+ va_end(args);
+
+ @throw [NSException exceptionWithName:name reason:reason userInfo:nil];
+}
+
+// return the property for a validated column name
+RLMProperty *RLMValidatedProperty(RLMObjectSchema *desc, NSString *columnName) {
+ RLMProperty *prop = desc[columnName];
+ RLMPrecondition(prop, @"Invalid property name",
+ @"Property '%@' not found in object of type '%@'", columnName, desc.className);
+ return prop;
+}
+
+namespace {
+BOOL RLMPropertyTypeIsNumeric(RLMPropertyType propertyType) {
+ switch (propertyType) {
+ case RLMPropertyTypeInt:
+ case RLMPropertyTypeFloat:
+ case RLMPropertyTypeDouble:
+ return YES;
+ default:
+ return NO;
+ }
+}
+
+// Equal and ContainsSubstring are used by QueryBuilder::add_string_constraint as the comparator
+// for performing diacritic-insensitive comparisons.
+
+bool equal(CFStringCompareFlags options, StringData v1, StringData v2)
+{
+ if (v1.is_null() || v2.is_null()) {
+ return v1.is_null() == v2.is_null();
+ }
+
+ auto s1 = util::adoptCF(CFStringCreateWithBytesNoCopy(kCFAllocatorSystemDefault, (const UInt8*)v1.data(), v1.size(),
+ kCFStringEncodingUTF8, false, kCFAllocatorNull));
+ auto s2 = util::adoptCF(CFStringCreateWithBytesNoCopy(kCFAllocatorSystemDefault, (const UInt8*)v2.data(), v2.size(),
+ kCFStringEncodingUTF8, false, kCFAllocatorNull));
+
+ return CFStringCompare(s1.get(), s2.get(), options) == kCFCompareEqualTo;
+}
+
+template <CFStringCompareFlags options>
+struct Equal {
+ using CaseSensitive = Equal<options & ~kCFCompareCaseInsensitive>;
+ using CaseInsensitive = Equal<options | kCFCompareCaseInsensitive>;
+
+ bool operator()(StringData v1, StringData v2, bool v1_null, bool v2_null) const
+ {
+ REALM_ASSERT_DEBUG(v1_null == v1.is_null());
+ REALM_ASSERT_DEBUG(v2_null == v2.is_null());
+
+ return equal(options, v1, v2);
+ }
+
+ // FIXME: Consider the options.
+ static const char* description() { return "equal"; }
+};
+
+bool contains_substring(CFStringCompareFlags options, StringData v1, StringData v2)
+{
+ if (v2.is_null()) {
+ // Everything contains NULL
+ return true;
+ }
+
+ if (v1.is_null()) {
+ // NULL contains nothing (except NULL, handled above)
+ return false;
+ }
+
+ if (v2.size() == 0) {
+ // Everything (except NULL, handled above) contains the empty string
+ return true;
+ }
+
+ auto s1 = util::adoptCF(CFStringCreateWithBytesNoCopy(kCFAllocatorSystemDefault, (const UInt8*)v1.data(), v1.size(),
+ kCFStringEncodingUTF8, false, kCFAllocatorNull));
+ auto s2 = util::adoptCF(CFStringCreateWithBytesNoCopy(kCFAllocatorSystemDefault, (const UInt8*)v2.data(), v2.size(),
+ kCFStringEncodingUTF8, false, kCFAllocatorNull));
+
+ return CFStringFind(s1.get(), s2.get(), options).location != kCFNotFound;
+}
+
+template <CFStringCompareFlags options>
+struct ContainsSubstring {
+ using CaseSensitive = ContainsSubstring<options & ~kCFCompareCaseInsensitive>;
+ using CaseInsensitive = ContainsSubstring<options | kCFCompareCaseInsensitive>;
+
+ bool operator()(StringData v1, StringData v2, bool v1_null, bool v2_null) const
+ {
+ REALM_ASSERT_DEBUG(v1_null == v1.is_null());
+ REALM_ASSERT_DEBUG(v2_null == v2.is_null());
+
+ return contains_substring(options, v1, v2);
+ }
+
+ // FIXME: Consider the options.
+ static const char* description() { return "contains"; }
+};
+
+
+NSString *operatorName(NSPredicateOperatorType operatorType)
+{
+ switch (operatorType) {
+ case NSLessThanPredicateOperatorType:
+ return @"<";
+ case NSLessThanOrEqualToPredicateOperatorType:
+ return @"<=";
+ case NSGreaterThanPredicateOperatorType:
+ return @">";
+ case NSGreaterThanOrEqualToPredicateOperatorType:
+ return @">=";
+ case NSEqualToPredicateOperatorType:
+ return @"==";
+ case NSNotEqualToPredicateOperatorType:
+ return @"!=";
+ case NSMatchesPredicateOperatorType:
+ return @"MATCHES";
+ case NSLikePredicateOperatorType:
+ return @"LIKE";
+ case NSBeginsWithPredicateOperatorType:
+ return @"BEGINSWITH";
+ case NSEndsWithPredicateOperatorType:
+ return @"ENDSWITH";
+ case NSInPredicateOperatorType:
+ return @"IN";
+ case NSContainsPredicateOperatorType:
+ return @"CONTAINS";
+ case NSBetweenPredicateOperatorType:
+ return @"BETWEEN";
+ case NSCustomSelectorPredicateOperatorType:
+ return @"custom selector";
+ }
+
+ return [NSString stringWithFormat:@"unknown operator %lu", (unsigned long)operatorType];
+}
+
+Table& get_table(Group& group, RLMObjectSchema *objectSchema)
+{
+ return *ObjectStore::table_for_object_type(group, objectSchema.objectName.UTF8String);
+}
+
+// A reference to a column within a query. Can be resolved to a Columns<T> for use in query expressions.
+class ColumnReference {
+public:
+ ColumnReference(Query& query, Group& group, RLMSchema *schema, RLMProperty* property, const std::vector<RLMProperty*>& links = {})
+ : m_links(links), m_property(property), m_schema(schema), m_group(&group), m_query(&query), m_table(query.get_table().get())
+ {
+ auto& table = walk_link_chain([](Table&, size_t, RLMPropertyType) { });
+ m_index = table.get_column_index(m_property.name.UTF8String);
+ }
+
+ template <typename T, typename... SubQuery>
+ auto resolve(SubQuery&&... subquery) const
+ {
+ static_assert(sizeof...(SubQuery) < 2, "resolve() takes at most one subquery");
+ set_link_chain_on_table();
+ if (type() != RLMPropertyTypeLinkingObjects) {
+ return m_table->template column<T>(index(), std::forward<SubQuery>(subquery)...);
+ }
+ else {
+ return resolve_backlink<T>(std::forward<SubQuery>(subquery)...);
+ }
+ }
+
+ RLMProperty *property() const { return m_property; }
+ size_t index() const { return m_index; }
+ RLMPropertyType type() const { return property().type; }
+ Group& group() const { return *m_group; }
+
+ RLMObjectSchema *link_target_object_schema() const
+ {
+ switch (type()) {
+ case RLMPropertyTypeObject:
+ case RLMPropertyTypeLinkingObjects:
+ return m_schema[property().objectClassName];
+ default:
+ REALM_UNREACHABLE();
+ }
+ }
+
+ bool has_links() const { return m_links.size(); }
+
+ bool has_any_to_many_links() const {
+ return std::any_of(begin(m_links), end(m_links),
+ [](RLMProperty *property) { return property.array; });
+ }
+
+ ColumnReference last_link_column() const {
+ REALM_ASSERT(!m_links.empty());
+ return {*m_query, *m_group, m_schema, m_links.back(), {m_links.begin(), m_links.end() - 1}};
+ }
+
+ ColumnReference column_ignoring_links(Query& query) const {
+ return {query, *m_group, m_schema, m_property};
+ }
+
+private:
+ template <typename T, typename... SubQuery>
+ auto resolve_backlink(SubQuery&&... subquery) const
+ {
+ // We actually just want `if constexpr (std::is_same<T, Link>::value) { ... }`,
+ // so fake it by tag-dispatching on the conditional
+ return do_resolve_backlink<T>(std::is_same<T, Link>(), std::forward<SubQuery>(subquery)...);
+ }
+
+ template <typename T, typename... SubQuery>
+ auto do_resolve_backlink(std::true_type, SubQuery&&... subquery) const
+ {
+ return with_link_origin(m_property, [&](Table& table, size_t col) {
+ return m_table->template column<T>(table, col, std::forward<SubQuery>(subquery)...);
+ });
+ }
+
+ template <typename T, typename... SubQuery>
+ Columns<T> do_resolve_backlink(std::false_type, SubQuery&&...) const
+ {
+ // This can't actually happen as we only call resolve_backlink() if
+ // it's RLMPropertyTypeLinkingObjects
+ __builtin_unreachable();
+ }
+
+ template<typename Func>
+ Table& walk_link_chain(Func&& func) const
+ {
+ auto table = m_query->get_table().get();
+ for (const auto& link : m_links) {
+ if (link.type != RLMPropertyTypeLinkingObjects) {
+ auto index = table->get_column_index(link.name.UTF8String);
+ func(*table, index, link.type);
+ table = table->get_link_target(index).get();
+ }
+ else {
+ with_link_origin(link, [&](Table& link_origin_table, size_t link_origin_column) {
+ func(link_origin_table, link_origin_column, link.type);
+ table = &link_origin_table;
+ });
+ }
+ }
+ return *table;
+ }
+
+ template<typename Func>
+ auto with_link_origin(RLMProperty *prop, Func&& func) const
+ {
+ RLMObjectSchema *link_origin_schema = m_schema[prop.objectClassName];
+ Table& link_origin_table = get_table(*m_group, link_origin_schema);
+ size_t link_origin_column = link_origin_table.get_column_index(prop.linkOriginPropertyName.UTF8String);
+ return func(link_origin_table, link_origin_column);
+ }
+
+ void set_link_chain_on_table() const
+ {
+ walk_link_chain([&](Table& current_table, size_t column, RLMPropertyType type) {
+ if (type == RLMPropertyTypeLinkingObjects) {
+ m_table->backlink(current_table, column);
+ }
+ else {
+ m_table->link(column);
+ }
+ });
+ }
+
+ std::vector<RLMProperty*> m_links;
+ RLMProperty *m_property;
+ RLMSchema *m_schema;
+ Group *m_group;
+ Query *m_query;
+ Table *m_table;
+ size_t m_index;
+};
+
+class CollectionOperation {
+public:
+ enum Type {
+ Count,
+ Minimum,
+ Maximum,
+ Sum,
+ Average,
+ };
+
+ CollectionOperation(Type type, ColumnReference link_column, util::Optional<ColumnReference> column)
+ : m_type(type)
+ , m_link_column(std::move(link_column))
+ , m_column(std::move(column))
+ {
+ RLMPrecondition(m_link_column.property().array,
+ @"Invalid predicate", @"Collection operation can only be applied to a property of type RLMArray.");
+
+ switch (m_type) {
+ case Count:
+ RLMPrecondition(!m_column, @"Invalid predicate", @"Result of @count does not have any properties.");
+ break;
+ case Minimum:
+ case Maximum:
+ case Sum:
+ case Average:
+ RLMPrecondition(m_column && RLMPropertyTypeIsNumeric(m_column->type()), @"Invalid predicate",
+ @"%@ can only be applied to a numeric property.", name_for_type(m_type));
+ break;
+ }
+ }
+
+ CollectionOperation(NSString *operationName, ColumnReference link_column, util::Optional<ColumnReference> column = util::none)
+ : CollectionOperation(type_for_name(operationName), std::move(link_column), std::move(column))
+ {
+ }
+
+ Type type() const { return m_type; }
+ const ColumnReference& link_column() const { return m_link_column; }
+ const ColumnReference& column() const { return *m_column; }
+
+ void validate_comparison(id value) const {
+ switch (m_type) {
+ case Count:
+ case Average:
+ RLMPrecondition([value isKindOfClass:[NSNumber class]], @"Invalid operand",
+ @"%@ can only be compared with a numeric value.", name_for_type(m_type));
+ break;
+ case Minimum:
+ case Maximum:
+ case Sum:
+ RLMPrecondition(RLMIsObjectValidForProperty(value, m_column->property()), @"Invalid operand",
+ @"%@ on a property of type %@ cannot be compared with '%@'",
+ name_for_type(m_type), RLMTypeToString(m_column->type()), value);
+ break;
+ }
+ }
+
+ void validate_comparison(const ColumnReference& column) const {
+ switch (m_type) {
+ case Count:
+ RLMPrecondition(RLMPropertyTypeIsNumeric(column.type()), @"Invalid operand",
+ @"%@ can only be compared with a numeric value.", name_for_type(m_type));
+ break;
+ case Average:
+ case Minimum:
+ case Maximum:
+ case Sum:
+ RLMPrecondition(RLMPropertyTypeIsNumeric(column.type()), @"Invalid operand",
+ @"%@ on a property of type %@ cannot be compared with property of type '%@'",
+ name_for_type(m_type), RLMTypeToString(m_column->type()), RLMTypeToString(column.type()));
+ break;
+ }
+ }
+
+private:
+ static Type type_for_name(NSString *name) {
+ if ([name isEqualToString:@"@count"]) {
+ return Count;
+ }
+ if ([name isEqualToString:@"@min"]) {
+ return Minimum;
+ }
+ if ([name isEqualToString:@"@max"]) {
+ return Maximum;
+ }
+ if ([name isEqualToString:@"@sum"]) {
+ return Sum;
+ }
+ if ([name isEqualToString:@"@avg"]) {
+ return Average;
+ }
+ @throw RLMPredicateException(@"Invalid predicate", @"Unsupported collection operation '%@'", name);
+ }
+
+ static NSString *name_for_type(Type type) {
+ switch (type) {
+ case Count: return @"@count";
+ case Minimum: return @"@min";
+ case Maximum: return @"@max";
+ case Sum: return @"@sum";
+ case Average: return @"@avg";
+ }
+ }
+
+ Type m_type;
+ ColumnReference m_link_column;
+ util::Optional<ColumnReference> m_column;
+};
+
+class QueryBuilder {
+public:
+ QueryBuilder(Query& query, Group& group, RLMSchema *schema)
+ : m_query(query), m_group(group), m_schema(schema) { }
+
+ void apply_predicate(NSPredicate *predicate, RLMObjectSchema *objectSchema);
+
+
+ void apply_collection_operator_expression(RLMObjectSchema *desc, NSString *keyPath, id value, NSComparisonPredicate *pred);
+ void apply_value_expression(RLMObjectSchema *desc, NSString *keyPath, id value, NSComparisonPredicate *pred);
+ void apply_column_expression(RLMObjectSchema *desc, NSString *leftKeyPath, NSString *rightKeyPath, NSComparisonPredicate *predicate);
+ void apply_subquery_count_expression(RLMObjectSchema *objectSchema, NSExpression *subqueryExpression,
+ NSPredicateOperatorType operatorType, NSExpression *right);
+ void apply_function_subquery_expression(RLMObjectSchema *objectSchema, NSExpression *functionExpression,
+ NSPredicateOperatorType operatorType, NSExpression *right);
+ void apply_function_expression(RLMObjectSchema *objectSchema, NSExpression *functionExpression,
+ NSPredicateOperatorType operatorType, NSExpression *right);
+
+
+ template <typename A, typename B>
+ void add_numeric_constraint(RLMPropertyType datatype,
+ NSPredicateOperatorType operatorType,
+ A&& lhs, B&& rhs);
+
+ template <typename A, typename B>
+ void add_bool_constraint(NSPredicateOperatorType operatorType, A lhs, B rhs);
+
+ void add_substring_constraint(null, Query condition);
+ template<typename T>
+ void add_substring_constraint(const T& value, Query condition);
+ template<typename T>
+ void add_substring_constraint(const Columns<T>& value, Query condition);
+
+ template <typename T>
+ void add_string_constraint(NSPredicateOperatorType operatorType,
+ NSComparisonPredicateOptions predicateOptions,
+ Columns<String> &&column,
+ T value);
+
+ void add_string_constraint(NSPredicateOperatorType operatorType,
+ NSComparisonPredicateOptions predicateOptions,
+ StringData value,
+ Columns<String>&& column);
+
+ template <typename L, typename R>
+ void add_constraint(RLMPropertyType type,
+ NSPredicateOperatorType operatorType,
+ NSComparisonPredicateOptions predicateOptions,
+ L lhs, R rhs);
+ template <typename... T>
+ void do_add_constraint(RLMPropertyType type, NSPredicateOperatorType operatorType,
+ NSComparisonPredicateOptions predicateOptions, T... values);
+ void do_add_constraint(RLMPropertyType, NSPredicateOperatorType, NSComparisonPredicateOptions, id, realm::null);
+
+ void add_between_constraint(const ColumnReference& column, id value);
+
+ void add_binary_constraint(NSPredicateOperatorType operatorType, const ColumnReference& column, BinaryData value);
+ void add_binary_constraint(NSPredicateOperatorType operatorType, const ColumnReference& column, id value);
+ void add_binary_constraint(NSPredicateOperatorType operatorType, const ColumnReference& column, null);
+ void add_binary_constraint(NSPredicateOperatorType operatorType, id value, const ColumnReference& column);
+ void add_binary_constraint(NSPredicateOperatorType, const ColumnReference&, const ColumnReference&);
+
+ void add_link_constraint(NSPredicateOperatorType operatorType, const ColumnReference& column, RLMObject *obj);
+ void add_link_constraint(NSPredicateOperatorType operatorType, const ColumnReference& column, realm::null);
+ template<typename T>
+ void add_link_constraint(NSPredicateOperatorType operatorType, T obj, const ColumnReference& column);
+ void add_link_constraint(NSPredicateOperatorType, const ColumnReference&, const ColumnReference&);
+
+ template <CollectionOperation::Type Operation, typename... T>
+ void add_collection_operation_constraint(RLMPropertyType propertyType, NSPredicateOperatorType operatorType, T... values);
+ template <typename... T>
+ void add_collection_operation_constraint(NSPredicateOperatorType operatorType,
+ CollectionOperation collectionOperation, T... values);
+
+
+ CollectionOperation collection_operation_from_key_path(RLMObjectSchema *desc, NSString *keyPath);
+ ColumnReference column_reference_from_key_path(RLMObjectSchema *objectSchema, NSString *keyPath, bool isAggregate);
+
+private:
+ Query& m_query;
+ Group& m_group;
+ RLMSchema *m_schema;
+};
+
+// add a clause for numeric constraints based on operator type
+template <typename A, typename B>
+void QueryBuilder::add_numeric_constraint(RLMPropertyType datatype,
+ NSPredicateOperatorType operatorType,
+ A&& lhs, B&& rhs)
+{
+ switch (operatorType) {
+ case NSLessThanPredicateOperatorType:
+ m_query.and_query(lhs < rhs);
+ break;
+ case NSLessThanOrEqualToPredicateOperatorType:
+ m_query.and_query(lhs <= rhs);
+ break;
+ case NSGreaterThanPredicateOperatorType:
+ m_query.and_query(lhs > rhs);
+ break;
+ case NSGreaterThanOrEqualToPredicateOperatorType:
+ m_query.and_query(lhs >= rhs);
+ break;
+ case NSEqualToPredicateOperatorType:
+ m_query.and_query(lhs == rhs);
+ break;
+ case NSNotEqualToPredicateOperatorType:
+ m_query.and_query(lhs != rhs);
+ break;
+ default:
+ @throw RLMPredicateException(@"Invalid operator type",
+ @"Operator '%@' not supported for type %@",
+ operatorName(operatorType), RLMTypeToString(datatype));
+ }
+}
+
+template <typename A, typename B>
+void QueryBuilder::add_bool_constraint(NSPredicateOperatorType operatorType, A lhs, B rhs) {
+ switch (operatorType) {
+ case NSEqualToPredicateOperatorType:
+ m_query.and_query(lhs == rhs);
+ break;
+ case NSNotEqualToPredicateOperatorType:
+ m_query.and_query(lhs != rhs);
+ break;
+ default:
+ @throw RLMPredicateException(@"Invalid operator type",
+ @"Operator '%@' not supported for bool type", operatorName(operatorType));
+ }
+}
+
+void QueryBuilder::add_substring_constraint(null, Query) {
+ // Foundation always returns false for substring operations with a RHS of null or "".
+ m_query.and_query(std::unique_ptr<Expression>(new FalseExpression));
+}
+
+template<typename T>
+void QueryBuilder::add_substring_constraint(const T& value, Query condition) {
+ // Foundation always returns false for substring operations with a RHS of null or "".
+ m_query.and_query(value.size()
+ ? std::move(condition)
+ : std::unique_ptr<Expression>(new FalseExpression));
+}
+
+template<typename T>
+void QueryBuilder::add_substring_constraint(const Columns<T>& value, Query condition) {
+ // Foundation always returns false for substring operations with a RHS of null or "".
+ // We don't need to concern ourselves with the possibility of value traversing a link list
+ // and producing multiple values per row as such expressions will have been rejected.
+ m_query.and_query(const_cast<Columns<String>&>(value).size() != 0 && std::move(condition));
+}
+
+template <typename T>
+void QueryBuilder::add_string_constraint(NSPredicateOperatorType operatorType,
+ NSComparisonPredicateOptions predicateOptions,
+ Columns<String> &&column,
+ T value) {
+ bool caseSensitive = !(predicateOptions & NSCaseInsensitivePredicateOption);
+ bool diacriticSensitive = !(predicateOptions & NSDiacriticInsensitivePredicateOption);
+
+ if (diacriticSensitive) {
+ switch (operatorType) {
+ case NSBeginsWithPredicateOperatorType:
+ add_substring_constraint(value, column.begins_with(value, caseSensitive));
+ break;
+ case NSEndsWithPredicateOperatorType:
+ add_substring_constraint(value, column.ends_with(value, caseSensitive));
+ break;
+ case NSContainsPredicateOperatorType:
+ add_substring_constraint(value, column.contains(value, caseSensitive));
+ break;
+ case NSEqualToPredicateOperatorType:
+ m_query.and_query(column.equal(value, caseSensitive));
+ break;
+ case NSNotEqualToPredicateOperatorType:
+ m_query.and_query(column.not_equal(value, caseSensitive));
+ break;
+ case NSLikePredicateOperatorType:
+ m_query.and_query(column.like(value, caseSensitive));
+ break;
+ default:
+ @throw RLMPredicateException(@"Invalid operator type",
+ @"Operator '%@' not supported for string type",
+ operatorName(operatorType));
+ }
+ return;
+ }
+
+ auto as_subexpr = util::overload([](StringData value) { return make_subexpr<ConstantStringValue>(value); },
+ [](const Columns<String>& c) { return c.clone(); });
+ auto left = as_subexpr(column);
+ auto right = as_subexpr(value);
+
+ auto make_constraint = [&](auto comparator) {
+ using Comparator = decltype(comparator);
+ using CompareCS = Compare<typename Comparator::CaseSensitive, StringData>;
+ using CompareCI = Compare<typename Comparator::CaseInsensitive, StringData>;
+ if (caseSensitive) {
+ return make_expression<CompareCS>(std::move(left), std::move(right));
+ }
+ else {
+ return make_expression<CompareCI>(std::move(left), std::move(right));
+ }
+ };
+
+ switch (operatorType) {
+ case NSBeginsWithPredicateOperatorType: {
+ using C = ContainsSubstring<kCFCompareDiacriticInsensitive | kCFCompareAnchored>;
+ add_substring_constraint(value, make_constraint(C{}));
+ break;
+ }
+ case NSEndsWithPredicateOperatorType: {
+ using C = ContainsSubstring<kCFCompareDiacriticInsensitive | kCFCompareAnchored | kCFCompareBackwards>;
+ add_substring_constraint(value, make_constraint(C{}));
+ break;
+ }
+ case NSContainsPredicateOperatorType: {
+ using C = ContainsSubstring<kCFCompareDiacriticInsensitive>;
+ add_substring_constraint(value, make_constraint(C{}));
+ break;
+ }
+ case NSNotEqualToPredicateOperatorType:
+ m_query.Not();
+ REALM_FALLTHROUGH;
+ case NSEqualToPredicateOperatorType:
+ m_query.and_query(make_constraint(Equal<kCFCompareDiacriticInsensitive>{}));
+ break;
+ case NSLikePredicateOperatorType:
+ @throw RLMPredicateException(@"Invalid operator type",
+ @"Operator 'LIKE' not supported with diacritic-insensitive modifier.");
+ default:
+ @throw RLMPredicateException(@"Invalid operator type",
+ @"Operator '%@' not supported for string type", operatorName(operatorType));
+ }
+}
+
+void QueryBuilder::add_string_constraint(NSPredicateOperatorType operatorType,
+ NSComparisonPredicateOptions predicateOptions,
+ StringData value,
+ Columns<String>&& column) {
+ switch (operatorType) {
+ case NSEqualToPredicateOperatorType:
+ case NSNotEqualToPredicateOperatorType:
+ add_string_constraint(operatorType, predicateOptions, std::move(column), value);
+ break;
+ default:
+ @throw RLMPredicateException(@"Invalid operator type",
+ @"Operator '%@' is not supported for string type with key path on right side of operator",
+ operatorName(operatorType));
+ }
+}
+
+id value_from_constant_expression_or_value(id value) {
+ if (NSExpression *exp = RLMDynamicCast<NSExpression>(value)) {
+ RLMPrecondition(exp.expressionType == NSConstantValueExpressionType,
+ @"Invalid value",
+ @"Expressions within predicate aggregates must be constant values");
+ return exp.constantValue;
+ }
+ return value;
+}
+
+void validate_and_extract_between_range(id value, RLMProperty *prop, id *from, id *to) {
+ NSArray *array = RLMDynamicCast<NSArray>(value);
+ RLMPrecondition(array, @"Invalid value", @"object must be of type NSArray for BETWEEN operations");
+ RLMPrecondition(array.count == 2, @"Invalid value", @"NSArray object must contain exactly two objects for BETWEEN operations");
+
+ *from = value_from_constant_expression_or_value(array.firstObject);
+ *to = value_from_constant_expression_or_value(array.lastObject);
+ RLMPrecondition(RLMIsObjectValidForProperty(*from, prop) && RLMIsObjectValidForProperty(*to, prop),
+ @"Invalid value",
+ @"NSArray objects must be of type %@ for BETWEEN operations", RLMTypeToString(prop.type));
+}
+
+void QueryBuilder::add_between_constraint(const ColumnReference& column, id value) {
+ if (column.has_any_to_many_links()) {
+ auto link_column = column.last_link_column();
+ Query subquery = get_table(m_group, link_column.link_target_object_schema()).where();
+ QueryBuilder(subquery, m_group, m_schema).add_between_constraint(column.column_ignoring_links(subquery), value);
+
+ m_query.and_query(link_column.resolve<Link>(std::move(subquery)).count() > 0);
+ return;
+ }
+
+ id from, to;
+ validate_and_extract_between_range(value, column.property(), &from, &to);
+
+ RLMPropertyType type = column.type();
+
+ m_query.group();
+ add_constraint(type, NSGreaterThanOrEqualToPredicateOperatorType, 0, column, from);
+ add_constraint(type, NSLessThanOrEqualToPredicateOperatorType, 0, column, to);
+ m_query.end_group();
+}
+
+void QueryBuilder::add_binary_constraint(NSPredicateOperatorType operatorType,
+ const ColumnReference& column,
+ BinaryData value) {
+ RLMPrecondition(!column.has_links(), @"Unsupported operator", @"NSData properties cannot be queried over an object link.");
+
+ size_t index = column.index();
+ Query query = m_query.get_table()->where();
+
+ switch (operatorType) {
+ case NSBeginsWithPredicateOperatorType:
+ add_substring_constraint(value, query.begins_with(index, value));
+ break;
+ case NSEndsWithPredicateOperatorType:
+ add_substring_constraint(value, query.ends_with(index, value));
+ break;
+ case NSContainsPredicateOperatorType:
+ add_substring_constraint(value, query.contains(index, value));
+ break;
+ case NSEqualToPredicateOperatorType:
+ m_query.equal(index, value);
+ break;
+ case NSNotEqualToPredicateOperatorType:
+ m_query.not_equal(index, value);
+ break;
+ default:
+ @throw RLMPredicateException(@"Invalid operator type",
+ @"Operator '%@' not supported for binary type", operatorName(operatorType));
+ }
+}
+
+void QueryBuilder::add_binary_constraint(NSPredicateOperatorType operatorType, const ColumnReference& column, id value) {
+ add_binary_constraint(operatorType, column, RLMBinaryDataForNSData(value));
+}
+
+void QueryBuilder::add_binary_constraint(NSPredicateOperatorType operatorType, const ColumnReference& column, null) {
+ add_binary_constraint(operatorType, column, BinaryData());
+}
+
+void QueryBuilder::add_binary_constraint(NSPredicateOperatorType operatorType, id value, const ColumnReference& column) {
+ switch (operatorType) {
+ case NSEqualToPredicateOperatorType:
+ case NSNotEqualToPredicateOperatorType:
+ add_binary_constraint(operatorType, column, value);
+ break;
+ default:
+ @throw RLMPredicateException(@"Invalid operator type",
+ @"Operator '%@' is not supported for binary type with key path on right side of operator",
+ operatorName(operatorType));
+ }
+}
+
+void QueryBuilder::add_binary_constraint(NSPredicateOperatorType, const ColumnReference&, const ColumnReference&) {
+ @throw RLMPredicateException(@"Invalid predicate", @"Comparisons between two NSData properties are not supported");
+}
+
+void QueryBuilder::add_link_constraint(NSPredicateOperatorType operatorType,
+ const ColumnReference& column, RLMObject *obj) {
+ RLMPrecondition(operatorType == NSEqualToPredicateOperatorType || operatorType == NSNotEqualToPredicateOperatorType,
+ @"Invalid operator type", @"Only 'Equal' and 'Not Equal' operators supported for object comparison");
+
+ if (operatorType == NSEqualToPredicateOperatorType) {
+ m_query.and_query(column.resolve<Link>() == obj->_row);
+ }
+ else {
+ m_query.and_query(column.resolve<Link>() != obj->_row);
+ }
+}
+
+void QueryBuilder::add_link_constraint(NSPredicateOperatorType operatorType,
+ const ColumnReference& column,
+ realm::null) {
+ RLMPrecondition(operatorType == NSEqualToPredicateOperatorType || operatorType == NSNotEqualToPredicateOperatorType,
+ @"Invalid operator type", @"Only 'Equal' and 'Not Equal' operators supported for object comparison");
+
+ if (operatorType == NSEqualToPredicateOperatorType) {
+ m_query.and_query(column.resolve<Link>() == null());
+ }
+ else {
+ m_query.and_query(column.resolve<Link>() != null());
+ }
+}
+
+template<typename T>
+void QueryBuilder::add_link_constraint(NSPredicateOperatorType operatorType, T obj, const ColumnReference& column) {
+ // Link constraints only support the equal-to and not-equal-to operators. The order of operands
+ // is not important for those comparisons so we can delegate to the other implementation.
+ add_link_constraint(operatorType, column, obj);
+}
+
+void QueryBuilder::add_link_constraint(NSPredicateOperatorType, const ColumnReference&, const ColumnReference&) {
+ // This is not actually reachable as this case is caught earlier, but this
+ // overload is needed for the code to compile
+ @throw RLMPredicateException(@"Invalid predicate", @"Comparisons between two RLMArray properties are not supported");
+}
+
+
+// iterate over an array of subpredicates, using @func to build a query from each
+// one and ORing them together
+template<typename Func>
+void process_or_group(Query &query, id array, Func&& func) {
+ RLMPrecondition([array conformsToProtocol:@protocol(NSFastEnumeration)],
+ @"Invalid value", @"IN clause requires an array of items");
+
+ query.group();
+
+ bool first = true;
+ for (id item in array) {
+ if (!first) {
+ query.Or();
+ }
+ first = false;
+
+ func(item);
+ }
+
+ if (first) {
+ // Queries can't be empty, so if there's zero things in the OR group
+ // validation will fail. Work around this by adding an expression which
+ // will never find any rows in a table.
+ query.and_query(std::unique_ptr<Expression>(new FalseExpression));
+ }
+
+ query.end_group();
+}
+
+template <typename RequestedType>
+RequestedType convert(id value);
+
+template <>
+Timestamp convert<Timestamp>(id value) {
+ return RLMTimestampForNSDate(value);
+}
+
+template <>
+bool convert<bool>(id value) {
+ return [value boolValue];
+}
+
+template <>
+Double convert<Double>(id value) {
+ return [value doubleValue];
+}
+
+template <>
+Float convert<Float>(id value) {
+ return [value floatValue];
+}
+
+template <>
+Int convert<Int>(id value) {
+ return [value longLongValue];
+}
+
+template <>
+String convert<String>(id value) {
+ return RLMStringDataWithNSString(value);
+}
+
+template <typename>
+realm::null value_of_type(realm::null) {
+ return realm::null();
+}
+
+template <typename RequestedType>
+auto value_of_type(id value) {
+ return ::convert<RequestedType>(value);
+}
+
+template <typename RequestedType>
+auto value_of_type(const ColumnReference& column) {
+ return column.resolve<RequestedType>();
+}
+
+
+template <typename... T>
+void QueryBuilder::do_add_constraint(RLMPropertyType type, NSPredicateOperatorType operatorType,
+ NSComparisonPredicateOptions predicateOptions, T... values)
+{
+ static_assert(sizeof...(T) == 2, "do_add_constraint accepts only two values as arguments");
+
+ switch (type) {
+ case RLMPropertyTypeBool:
+ add_bool_constraint(operatorType, value_of_type<bool>(values)...);
+ break;
+ case RLMPropertyTypeDate:
+ add_numeric_constraint(type, operatorType, value_of_type<realm::Timestamp>(values)...);
+ break;
+ case RLMPropertyTypeDouble:
+ add_numeric_constraint(type, operatorType, value_of_type<Double>(values)...);
+ break;
+ case RLMPropertyTypeFloat:
+ add_numeric_constraint(type, operatorType, value_of_type<Float>(values)...);
+ break;
+ case RLMPropertyTypeInt:
+ add_numeric_constraint(type, operatorType, value_of_type<Int>(values)...);
+ break;
+ case RLMPropertyTypeString:
+ add_string_constraint(operatorType, predicateOptions, value_of_type<String>(values)...);
+ break;
+ case RLMPropertyTypeData:
+ add_binary_constraint(operatorType, values...);
+ break;
+ case RLMPropertyTypeObject:
+ case RLMPropertyTypeLinkingObjects:
+ add_link_constraint(operatorType, values...);
+ break;
+ default:
+ @throw RLMPredicateException(@"Unsupported predicate value type",
+ @"Object type %@ not supported", RLMTypeToString(type));
+ }
+}
+
+void QueryBuilder::do_add_constraint(RLMPropertyType, NSPredicateOperatorType, NSComparisonPredicateOptions, id, realm::null)
+{
+ // This is not actually reachable as this case is caught earlier, but this
+ // overload is needed for the code to compile
+ @throw RLMPredicateException(@"Invalid predicate expressions",
+ @"Predicate expressions must compare a keypath and another keypath or a constant value");
+}
+
+bool is_nsnull(id value) {
+ return !value || value == NSNull.null;
+}
+
+template<typename T>
+bool is_nsnull(T) {
+ return false;
+}
+
+template <typename L, typename R>
+void QueryBuilder::add_constraint(RLMPropertyType type, NSPredicateOperatorType operatorType,
+ NSComparisonPredicateOptions predicateOptions, L lhs, R rhs)
+{
+ // The expression operators are only overloaded for realm::null on the rhs
+ RLMPrecondition(!is_nsnull(lhs), @"Unsupported operator",
+ @"Nil is only supported on the right side of operators");
+
+ if (is_nsnull(rhs)) {
+ do_add_constraint(type, operatorType, predicateOptions, lhs, realm::null());
+ }
+ else {
+ do_add_constraint(type, operatorType, predicateOptions, lhs, rhs);
+ }
+}
+
+struct KeyPath {
+ std::vector<RLMProperty *> links;
+ RLMProperty *property;
+ bool containsToManyRelationship;
+};
+
+KeyPath key_path_from_string(RLMSchema *schema, RLMObjectSchema *objectSchema, NSString *keyPath)
+{
+ RLMProperty *property;
+ std::vector<RLMProperty *> links;
+
+ bool keyPathContainsToManyRelationship = false;
+
+ NSUInteger start = 0, length = keyPath.length, end = NSNotFound;
+ do {
+ end = [keyPath rangeOfString:@"." options:0 range:{start, length - start}].location;
+ NSString *propertyName = [keyPath substringWithRange:{start, end == NSNotFound ? length - start : end - start}];
+ property = objectSchema[propertyName];
+ RLMPrecondition(property, @"Invalid property name",
+ @"Property '%@' not found in object of type '%@'",
+ propertyName, objectSchema.className);
+
+ if (property.array)
+ keyPathContainsToManyRelationship = true;
+
+ if (end != NSNotFound) {
+ RLMPrecondition(property.type == RLMPropertyTypeObject || property.type == RLMPropertyTypeLinkingObjects,
+ @"Invalid value", @"Property '%@' is not a link in object of type '%@'",
+ propertyName, objectSchema.className);
+
+ links.push_back(property);
+ REALM_ASSERT(property.objectClassName);
+ objectSchema = schema[property.objectClassName];
+ }
+
+ start = end + 1;
+ } while (end != NSNotFound);
+
+ return {std::move(links), property, keyPathContainsToManyRelationship};
+}
+
+ColumnReference QueryBuilder::column_reference_from_key_path(RLMObjectSchema *objectSchema,
+ NSString *keyPathString, bool isAggregate)
+{
+ auto keyPath = key_path_from_string(m_schema, objectSchema, keyPathString);
+
+ if (isAggregate && !keyPath.containsToManyRelationship) {
+ @throw RLMPredicateException(@"Invalid predicate",
+ @"Aggregate operations can only be used on key paths that include an array property");
+ } else if (!isAggregate && keyPath.containsToManyRelationship) {
+ @throw RLMPredicateException(@"Invalid predicate",
+ @"Key paths that include an array property must use aggregate operations");
+ }
+
+ return ColumnReference(m_query, m_group, m_schema, keyPath.property, std::move(keyPath.links));
+}
+
+void validate_property_value(const ColumnReference& column,
+ __unsafe_unretained id const value,
+ __unsafe_unretained NSString *const err,
+ __unsafe_unretained RLMObjectSchema *const objectSchema,
+ __unsafe_unretained NSString *const keyPath) {
+ RLMProperty *prop = column.property();
+ if (prop.array) {
+ RLMPrecondition([RLMObjectBaseObjectSchema(RLMDynamicCast<RLMObjectBase>(value)).className isEqualToString:prop.objectClassName],
+ @"Invalid value", err, prop.objectClassName, keyPath, objectSchema.className, value);
+ }
+ else {
+ RLMPrecondition(RLMIsObjectValidForProperty(value, prop),
+ @"Invalid value", err, RLMTypeToString(prop.type), keyPath, objectSchema.className, value);
+ }
+ if (RLMObjectBase *obj = RLMDynamicCast<RLMObjectBase>(value)) {
+ RLMPrecondition(!obj->_row.is_attached() || &column.group() == &obj->_realm.group,
+ @"Invalid value origin", @"Object must be from the Realm being queried");
+ }
+}
+
+template <typename RequestedType, CollectionOperation::Type OperationType>
+struct ValueOfTypeWithCollectionOperationHelper;
+
+template <>
+struct ValueOfTypeWithCollectionOperationHelper<Int, CollectionOperation::Count> {
+ static auto convert(const CollectionOperation& operation)
+ {
+ assert(operation.type() == CollectionOperation::Count);
+ return operation.link_column().resolve<Link>().count();
+ }
+};
+
+#define VALUE_OF_TYPE_WITH_COLLECTION_OPERATOR_HELPER(OperationType, function) \
+template <typename T> \
+struct ValueOfTypeWithCollectionOperationHelper<T, OperationType> { \
+ static auto convert(const CollectionOperation& operation) \
+ { \
+ REALM_ASSERT(operation.type() == OperationType); \
+ auto targetColumn = operation.link_column().resolve<Link>().template column<T>(operation.column().index()); \
+ return targetColumn.function(); \
+ } \
+} \
+
+VALUE_OF_TYPE_WITH_COLLECTION_OPERATOR_HELPER(CollectionOperation::Minimum, min);
+VALUE_OF_TYPE_WITH_COLLECTION_OPERATOR_HELPER(CollectionOperation::Maximum, max);
+VALUE_OF_TYPE_WITH_COLLECTION_OPERATOR_HELPER(CollectionOperation::Sum, sum);
+VALUE_OF_TYPE_WITH_COLLECTION_OPERATOR_HELPER(CollectionOperation::Average, average);
+#undef VALUE_OF_TYPE_WITH_COLLECTION_OPERATOR_HELPER
+
+template <typename Requested, CollectionOperation::Type OperationType, typename T>
+auto value_of_type_with_collection_operation(T&& value) {
+ return value_of_type<Requested>(std::forward<T>(value));
+}
+
+template <typename Requested, CollectionOperation::Type OperationType>
+auto value_of_type_with_collection_operation(CollectionOperation operation) {
+ using helper = ValueOfTypeWithCollectionOperationHelper<Requested, OperationType>;
+ return helper::convert(operation);
+}
+
+template <CollectionOperation::Type Operation, typename... T>
+void QueryBuilder::add_collection_operation_constraint(RLMPropertyType propertyType, NSPredicateOperatorType operatorType, T... values)
+{
+ switch (propertyType) {
+ case RLMPropertyTypeInt:
+ add_numeric_constraint(propertyType, operatorType, value_of_type_with_collection_operation<Int, Operation>(values)...);
+ break;
+ case RLMPropertyTypeFloat:
+ add_numeric_constraint(propertyType, operatorType, value_of_type_with_collection_operation<Float, Operation>(values)...);
+ break;
+ case RLMPropertyTypeDouble:
+ add_numeric_constraint(propertyType, operatorType, value_of_type_with_collection_operation<Double, Operation>(values)...);
+ break;
+ default:
+ REALM_ASSERT(false && "Only numeric property types should hit this path.");
+ }
+}
+
+template <typename... T>
+void QueryBuilder::add_collection_operation_constraint(NSPredicateOperatorType operatorType,
+ CollectionOperation collectionOperation, T... values)
+{
+ static_assert(sizeof...(T) == 2, "add_collection_operation_constraint accepts only two values as arguments");
+
+ switch (collectionOperation.type()) {
+ case CollectionOperation::Count:
+ add_numeric_constraint(RLMPropertyTypeInt, operatorType,
+ value_of_type_with_collection_operation<Int, CollectionOperation::Count>(values)...);
+ break;
+ case CollectionOperation::Minimum:
+ add_collection_operation_constraint<CollectionOperation::Minimum>(collectionOperation.column().type(), operatorType, values...);
+ break;
+ case CollectionOperation::Maximum:
+ add_collection_operation_constraint<CollectionOperation::Maximum>(collectionOperation.column().type(), operatorType, values...);
+ break;
+ case CollectionOperation::Sum:
+ add_collection_operation_constraint<CollectionOperation::Sum>(collectionOperation.column().type(), operatorType, values...);
+ break;
+ case CollectionOperation::Average:
+ add_collection_operation_constraint<CollectionOperation::Average>(collectionOperation.column().type(), operatorType, values...);
+ break;
+ }
+}
+
+bool key_path_contains_collection_operator(NSString *keyPath) {
+ return [keyPath rangeOfString:@"@"].location != NSNotFound;
+}
+
+NSString *get_collection_operation_name_from_key_path(NSString *keyPath, NSString **leadingKeyPath,
+ NSString **trailingKey) {
+ NSRange at = [keyPath rangeOfString:@"@"];
+ if (at.location == NSNotFound || at.location >= keyPath.length - 1) {
+ @throw RLMPredicateException(@"Invalid key path", @"'%@' is not a valid key path'", keyPath);
+ }
+
+ if (at.location == 0 || [keyPath characterAtIndex:at.location - 1] != '.') {
+ @throw RLMPredicateException(@"Invalid key path", @"'%@' is not a valid key path'", keyPath);
+ }
+
+ NSRange trailingKeyRange = [keyPath rangeOfString:@"." options:0 range:{at.location, keyPath.length - at.location} locale:nil];
+
+ *leadingKeyPath = [keyPath substringToIndex:at.location - 1];
+ if (trailingKeyRange.location == NSNotFound) {
+ *trailingKey = nil;
+ return [keyPath substringFromIndex:at.location];
+ } else {
+ *trailingKey = [keyPath substringFromIndex:trailingKeyRange.location + 1];
+ return [keyPath substringWithRange:{at.location, trailingKeyRange.location - at.location}];
+ }
+}
+
+CollectionOperation QueryBuilder::collection_operation_from_key_path(RLMObjectSchema *desc, NSString *keyPath) {
+ NSString *leadingKeyPath;
+ NSString *trailingKey;
+ NSString *collectionOperationName = get_collection_operation_name_from_key_path(keyPath, &leadingKeyPath, &trailingKey);
+
+ ColumnReference linkColumn = column_reference_from_key_path(desc, leadingKeyPath, true);
+ util::Optional<ColumnReference> column;
+ if (trailingKey) {
+ RLMPrecondition([trailingKey rangeOfString:@"."].location == NSNotFound, @"Invalid key path",
+ @"Right side of collection operator may only have a single level key");
+ NSString *fullKeyPath = [leadingKeyPath stringByAppendingFormat:@".%@", trailingKey];
+ column = column_reference_from_key_path(desc, fullKeyPath, true);
+ }
+
+ return {collectionOperationName, std::move(linkColumn), std::move(column)};
+}
+
+void QueryBuilder::apply_collection_operator_expression(RLMObjectSchema *desc,
+ NSString *keyPath, id value,
+ NSComparisonPredicate *pred) {
+ CollectionOperation operation = collection_operation_from_key_path(desc, keyPath);
+ operation.validate_comparison(value);
+
+ if (pred.leftExpression.expressionType == NSKeyPathExpressionType) {
+ add_collection_operation_constraint(pred.predicateOperatorType, operation, operation, value);
+ } else {
+ add_collection_operation_constraint(pred.predicateOperatorType, operation, value, operation);
+ }
+}
+
+void QueryBuilder::apply_value_expression(RLMObjectSchema *desc,
+ NSString *keyPath, id value,
+ NSComparisonPredicate *pred)
+{
+ if (key_path_contains_collection_operator(keyPath)) {
+ apply_collection_operator_expression(desc, keyPath, value, pred);
+ return;
+ }
+
+ bool isAny = pred.comparisonPredicateModifier == NSAnyPredicateModifier;
+ ColumnReference column = column_reference_from_key_path(desc, keyPath, isAny);
+
+ // check to see if this is a between query
+ if (pred.predicateOperatorType == NSBetweenPredicateOperatorType) {
+ add_between_constraint(std::move(column), value);
+ return;
+ }
+
+ // turn "key.path IN collection" into ored together ==. "collection IN key.path" is handled elsewhere.
+ if (pred.predicateOperatorType == NSInPredicateOperatorType) {
+ process_or_group(m_query, value, [&](id item) {
+ id normalized = value_from_constant_expression_or_value(item);
+ validate_property_value(column, normalized,
+ @"Expected object of type %@ in IN clause for property '%@' on object of type '%@', but received: %@", desc, keyPath);
+ add_constraint(column.type(), NSEqualToPredicateOperatorType, pred.options, column, normalized);
+ });
+ return;
+ }
+
+ validate_property_value(column, value, @"Expected object of type %@ for property '%@' on object of type '%@', but received: %@", desc, keyPath);
+ if (pred.leftExpression.expressionType == NSKeyPathExpressionType) {
+ add_constraint(column.type(), pred.predicateOperatorType, pred.options, std::move(column), value);
+ } else {
+ add_constraint(column.type(), pred.predicateOperatorType, pred.options, value, std::move(column));
+ }
+}
+
+void QueryBuilder::apply_column_expression(RLMObjectSchema *desc,
+ NSString *leftKeyPath, NSString *rightKeyPath,
+ NSComparisonPredicate *predicate)
+{
+ bool left_key_path_contains_collection_operator = key_path_contains_collection_operator(leftKeyPath);
+ bool right_key_path_contains_collection_operator = key_path_contains_collection_operator(rightKeyPath);
+ if (left_key_path_contains_collection_operator && right_key_path_contains_collection_operator) {
+ @throw RLMPredicateException(@"Unsupported predicate", @"Key paths including aggregate operations cannot be compared with other aggregate operations.");
+ }
+
+ if (left_key_path_contains_collection_operator) {
+ CollectionOperation left = collection_operation_from_key_path(desc, leftKeyPath);
+ ColumnReference right = column_reference_from_key_path(desc, rightKeyPath, false);
+ left.validate_comparison(right);
+ add_collection_operation_constraint(predicate.predicateOperatorType, left, left, std::move(right));
+ return;
+ }
+ if (right_key_path_contains_collection_operator) {
+ ColumnReference left = column_reference_from_key_path(desc, leftKeyPath, false);
+ CollectionOperation right = collection_operation_from_key_path(desc, rightKeyPath);
+ right.validate_comparison(left);
+ add_collection_operation_constraint(predicate.predicateOperatorType, right, std::move(left), right);
+ return;
+ }
+
+ bool isAny = false;
+ ColumnReference left = column_reference_from_key_path(desc, leftKeyPath, isAny);
+ ColumnReference right = column_reference_from_key_path(desc, rightKeyPath, isAny);
+
+ // NOTE: It's assumed that column type must match and no automatic type conversion is supported.
+ RLMPrecondition(left.type() == right.type(),
+ RLMPropertiesComparisonTypeMismatchException,
+ RLMPropertiesComparisonTypeMismatchReason,
+ RLMTypeToString(left.type()),
+ RLMTypeToString(right.type()));
+
+ // TODO: Should we handle special case where left row is the same as right row (tautology)
+ add_constraint(left.type(), predicate.predicateOperatorType, predicate.options,
+ std::move(left), std::move(right));
+}
+
+// Identify expressions of the form [SELF valueForKeyPath:]
+bool is_self_value_for_key_path_function_expression(NSExpression *expression)
+{
+ if (expression.expressionType != NSFunctionExpressionType)
+ return false;
+
+ if (expression.operand.expressionType != NSEvaluatedObjectExpressionType)
+ return false;
+
+ return [expression.function isEqualToString:@"valueForKeyPath:"];
+}
+
+// -[NSPredicate predicateWithSubtitutionVariables:] results in function expressions of the form [SELF valueForKeyPath:]
+// that apply_predicate cannot handle. Replace such expressions with equivalent NSKeyPathExpressionType expressions.
+NSExpression *simplify_self_value_for_key_path_function_expression(NSExpression *expression) {
+ if (is_self_value_for_key_path_function_expression(expression)) {
+ if (NSString *keyPath = [expression.arguments.firstObject keyPath]) {
+ return [NSExpression expressionForKeyPath:keyPath];
+ }
+ }
+ return expression;
+}
+
+void QueryBuilder::apply_subquery_count_expression(RLMObjectSchema *objectSchema,
+ NSExpression *subqueryExpression, NSPredicateOperatorType operatorType, NSExpression *right) {
+ if (right.expressionType != NSConstantValueExpressionType || ![right.constantValue isKindOfClass:[NSNumber class]]) {
+ @throw RLMPredicateException(@"Invalid predicate expression", @"SUBQUERY(…).@count is only supported when compared with a constant number.");
+ }
+ int64_t value = [right.constantValue integerValue];
+
+ ColumnReference collectionColumn = column_reference_from_key_path(objectSchema, [subqueryExpression.collection keyPath], true);
+ RLMObjectSchema *collectionMemberObjectSchema = m_schema[collectionColumn.property().objectClassName];
+
+ // Eliminate references to the iteration variable in the subquery.
+ NSPredicate *subqueryPredicate = [subqueryExpression.predicate predicateWithSubstitutionVariables:@{ subqueryExpression.variable : [NSExpression expressionForEvaluatedObject] }];
+ subqueryPredicate = transformPredicate(subqueryPredicate, simplify_self_value_for_key_path_function_expression);
+
+ Query subquery = RLMPredicateToQuery(subqueryPredicate, collectionMemberObjectSchema, m_schema, m_group);
+ add_numeric_constraint(RLMPropertyTypeInt, operatorType,
+ collectionColumn.resolve<LinkList>(std::move(subquery)).count(), value);
+}
+
+void QueryBuilder::apply_function_subquery_expression(RLMObjectSchema *objectSchema, NSExpression *functionExpression,
+ NSPredicateOperatorType operatorType, NSExpression *right) {
+ if (![functionExpression.function isEqualToString:@"valueForKeyPath:"] || functionExpression.arguments.count != 1) {
+ @throw RLMPredicateException(@"Invalid predicate", @"The '%@' function is not supported on the result of a SUBQUERY.", functionExpression.function);
+ }
+
+ NSExpression *keyPathExpression = functionExpression.arguments.firstObject;
+ if ([keyPathExpression.keyPath isEqualToString:@"@count"]) {
+ apply_subquery_count_expression(objectSchema, functionExpression.operand, operatorType, right);
+ } else {
+ @throw RLMPredicateException(@"Invalid predicate", @"SUBQUERY is only supported when immediately followed by .@count that is compared with a constant number.");
+ }
+}
+
+void QueryBuilder::apply_function_expression(RLMObjectSchema *objectSchema, NSExpression *functionExpression,
+ NSPredicateOperatorType operatorType, NSExpression *right) {
+ if (functionExpression.operand.expressionType == NSSubqueryExpressionType) {
+ apply_function_subquery_expression(objectSchema, functionExpression, operatorType, right);
+ } else {
+ @throw RLMPredicateException(@"Invalid predicate", @"The '%@' function is not supported.", functionExpression.function);
+ }
+}
+
+
+void QueryBuilder::apply_predicate(NSPredicate *predicate, RLMObjectSchema *objectSchema)
+{
+ // Compound predicates.
+ if ([predicate isMemberOfClass:[NSCompoundPredicate class]]) {
+ NSCompoundPredicate *comp = (NSCompoundPredicate *)predicate;
+
+ switch ([comp compoundPredicateType]) {
+ case NSAndPredicateType:
+ if (comp.subpredicates.count) {
+ // Add all of the subpredicates.
+ m_query.group();
+ for (NSPredicate *subp in comp.subpredicates) {
+ apply_predicate(subp, objectSchema);
+ }
+ m_query.end_group();
+ } else {
+ // NSCompoundPredicate's documentation states that an AND predicate with no subpredicates evaluates to TRUE.
+ m_query.and_query(std::unique_ptr<Expression>(new TrueExpression));
+ }
+ break;
+
+ case NSOrPredicateType: {
+ // Add all of the subpredicates with ors inbetween.
+ process_or_group(m_query, comp.subpredicates, [&](__unsafe_unretained NSPredicate *const subp) {
+ apply_predicate(subp, objectSchema);
+ });
+ break;
+ }
+
+ case NSNotPredicateType:
+ // Add the negated subpredicate
+ m_query.Not();
+ apply_predicate(comp.subpredicates.firstObject, objectSchema);
+ break;
+
+ default:
+ @throw RLMPredicateException(@"Invalid compound predicate type",
+ @"Only support AND, OR and NOT predicate types");
+ }
+ }
+ else if ([predicate isMemberOfClass:[NSComparisonPredicate class]]) {
+ NSComparisonPredicate *compp = (NSComparisonPredicate *)predicate;
+
+ // check modifier
+ RLMPrecondition(compp.comparisonPredicateModifier != NSAllPredicateModifier,
+ @"Invalid predicate", @"ALL modifier not supported");
+
+ NSExpressionType exp1Type = compp.leftExpression.expressionType;
+ NSExpressionType exp2Type = compp.rightExpression.expressionType;
+
+ if (compp.comparisonPredicateModifier == NSAnyPredicateModifier) {
+ // for ANY queries
+ RLMPrecondition(exp1Type == NSKeyPathExpressionType && exp2Type == NSConstantValueExpressionType,
+ @"Invalid predicate",
+ @"Predicate with ANY modifier must compare a KeyPath with RLMArray with a value");
+ }
+
+ if (compp.predicateOperatorType == NSBetweenPredicateOperatorType || compp.predicateOperatorType == NSInPredicateOperatorType) {
+ // Inserting an array via %@ gives NSConstantValueExpressionType, but including it directly gives NSAggregateExpressionType
+ if (exp1Type == NSKeyPathExpressionType && (exp2Type == NSAggregateExpressionType || exp2Type == NSConstantValueExpressionType)) {
+ // "key.path IN %@", "key.path IN {…}", "key.path BETWEEN %@", or "key.path BETWEEN {…}".
+ exp2Type = NSConstantValueExpressionType;
+ }
+ else if (compp.predicateOperatorType == NSInPredicateOperatorType && exp1Type == NSConstantValueExpressionType && exp2Type == NSKeyPathExpressionType) {
+ // "%@ IN key.path" is equivalent to "ANY key.path IN %@". Rewrite the former into the latter.
+ compp = [NSComparisonPredicate predicateWithLeftExpression:compp.rightExpression rightExpression:compp.leftExpression
+ modifier:NSAnyPredicateModifier type:NSEqualToPredicateOperatorType options:0];
+ exp1Type = NSKeyPathExpressionType;
+ exp2Type = NSConstantValueExpressionType;
+ }
+ else {
+ if (compp.predicateOperatorType == NSBetweenPredicateOperatorType) {
+ @throw RLMPredicateException(@"Invalid predicate",
+ @"Predicate with BETWEEN operator must compare a KeyPath with an aggregate with two values");
+ }
+ else if (compp.predicateOperatorType == NSInPredicateOperatorType) {
+ @throw RLMPredicateException(@"Invalid predicate",
+ @"Predicate with IN operator must compare a KeyPath with an aggregate");
+ }
+ }
+ }
+
+ if (exp1Type == NSKeyPathExpressionType && exp2Type == NSKeyPathExpressionType) {
+ // both expression are KeyPaths
+ apply_column_expression(objectSchema, compp.leftExpression.keyPath, compp.rightExpression.keyPath, compp);
+ }
+ else if (exp1Type == NSKeyPathExpressionType && exp2Type == NSConstantValueExpressionType) {
+ // comparing keypath to value
+ apply_value_expression(objectSchema, compp.leftExpression.keyPath, compp.rightExpression.constantValue, compp);
+ }
+ else if (exp1Type == NSConstantValueExpressionType && exp2Type == NSKeyPathExpressionType) {
+ // comparing value to keypath
+ apply_value_expression(objectSchema, compp.rightExpression.keyPath, compp.leftExpression.constantValue, compp);
+ }
+ else if (exp1Type == NSFunctionExpressionType) {
+ apply_function_expression(objectSchema, compp.leftExpression, compp.predicateOperatorType, compp.rightExpression);
+ }
+ else if (exp1Type == NSSubqueryExpressionType) {
+ // The subquery expressions that we support are handled by the NSFunctionExpressionType case above.
+ @throw RLMPredicateException(@"Invalid predicate expression", @"SUBQUERY is only supported when immediately followed by .@count.");
+ }
+ else {
+ @throw RLMPredicateException(@"Invalid predicate expressions",
+ @"Predicate expressions must compare a keypath and another keypath or a constant value");
+ }
+ }
+ else if ([predicate isEqual:[NSPredicate predicateWithValue:YES]]) {
+ m_query.and_query(std::unique_ptr<Expression>(new TrueExpression));
+ } else if ([predicate isEqual:[NSPredicate predicateWithValue:NO]]) {
+ m_query.and_query(std::unique_ptr<Expression>(new FalseExpression));
+ }
+ else {
+ // invalid predicate type
+ @throw RLMPredicateException(@"Invalid predicate",
+ @"Only support compound, comparison, and constant predicates");
+ }
+}
+} // namespace
+
+realm::Query RLMPredicateToQuery(NSPredicate *predicate, RLMObjectSchema *objectSchema,
+ RLMSchema *schema, Group &group)
+{
+ auto query = get_table(group, objectSchema).where();
+
+ // passing a nil predicate is a no-op
+ if (!predicate) {
+ return query;
+ }
+
+ @autoreleasepool {
+ QueryBuilder(query, group, schema).apply_predicate(predicate, objectSchema);
+ }
+
+ // Test the constructed query in core
+ std::string validateMessage = query.validate();
+ RLMPrecondition(validateMessage.empty(), @"Invalid query", @"%.*s",
+ (int)validateMessage.size(), validateMessage.c_str());
+ return query;
+}