1491 lines
65 KiB
Plaintext
1491 lines
65 KiB
Plaintext
////////////////////////////////////////////////////////////////////////////
|
|
//
|
|
// 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.columnName.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.columnName.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);
|
|
NSString *column_name = link_origin_schema[prop.linkOriginPropertyName].columnName;
|
|
size_t link_origin_column = link_origin_table.get_column_index(column_name.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;
|
|
}
|