Skip to content

Commit

Permalink
Add support for lexicographical comparison for string queries
Browse files Browse the repository at this point in the history
  • Loading branch information
dianaafanador3 committed Jun 27, 2024
1 parent e3258a2 commit 1fc78b2
Show file tree
Hide file tree
Showing 5 changed files with 312 additions and 5 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
x.y.z Release notes (yyyy-MM-dd)
=============================================================
### Enhancements
* None.
* Add support for string comparison queries, which allows building string
queries with the following operators (`>`, `>=`, `<`, `<=`).
This is a case sensitive lexicographical comparison.
([#8008](https://github.com/realm/realm-swift/issues/8008)).

### Fixed
* <How to hit and notice issue? what was the impact?> ([#????](https://github.com/realm/realm-swift/issues/????), since v?.?.?)
Expand Down
8 changes: 8 additions & 0 deletions Realm/RLMQueryUtil.mm
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,14 @@ Query make_diacritic_sensitive_constraint(NSPredicateOperatorType operatorType,
return column.not_equal(value, caseSensitive);
case NSLikePredicateOperatorType:
return column.like(value, caseSensitive);
case NSLessThanPredicateOperatorType:
return column < value;
case NSLessThanOrEqualToPredicateOperatorType:
return column <= value;
case NSGreaterThanPredicateOperatorType:
return column > value;
case NSGreaterThanOrEqualToPredicateOperatorType:
return column >= value;
default: {
if constexpr (is_any_v<C, Columns<String>, Columns<Lst<String>>, Columns<Set<String>>, ColumnDictionaryKeys>) {
unsupportedOperator(RLMPropertyTypeString, operatorType);
Expand Down
33 changes: 33 additions & 0 deletions RealmSwift/Query.swift
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,39 @@ extension Query where T: _HasPersistedType, T.PersistedType: _QueryBinary {
public func notEquals<U>(_ column: Query<U>, options: StringOptions = []) -> Query<Bool> {
.init(.comparison(operator: .notEqual, node, column.node, options: options))
}

/// :nodoc:
public static func > (_ lhs: Query, _ rhs: T) -> Query<Bool> {
.init(.comparison(operator: .greaterThan, lhs.node, .constant(rhs), options: []))
}
/// :nodoc:
public static func > (_ lhs: Query, _ rhs: Query) -> Query<Bool> {
.init(.comparison(operator: .greaterThan, lhs.node, rhs.node, options: []))
}
/// :nodoc:
public static func >= (_ lhs: Query, _ rhs: T) -> Query<Bool> {
.init(.comparison(operator: .greaterThanEqual, lhs.node, .constant(rhs), options: []))
}
/// :nodoc:
public static func >= (_ lhs: Query, _ rhs: Query) -> Query<Bool> {
.init(.comparison(operator: .greaterThanEqual, lhs.node, rhs.node, options: []))
}
/// :nodoc:
public static func < (_ lhs: Query, _ rhs: T) -> Query<Bool> {
.init(.comparison(operator: .lessThan, lhs.node, .constant(rhs), options: []))
}
/// :nodoc:
public static func < (_ lhs: Query, _ rhs: Query) -> Query<Bool> {
.init(.comparison(operator: .lessThan, lhs.node, rhs.node, options: []))
}
/// :nodoc:
public static func <= (_ lhs: Query, _ rhs: T) -> Query<Bool> {
.init(.comparison(operator: .lessThanEqual, lhs.node, .constant(rhs), options: []))
}
/// :nodoc:
public static func <= (_ lhs: Query, _ rhs: Query) -> Query<Bool> {
.init(.comparison(operator: .lessThanEqual, lhs.node, rhs.node, options: []))
}
}

extension Query where T: OptionalProtocol, T.Wrapped: Comparable {
Expand Down
139 changes: 137 additions & 2 deletions RealmSwift/Tests/QueryTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1738,7 +1738,37 @@ class QueryTests: TestCase, @unchecked Sendable {
validateNumericComparisons("optDecimal", \Query<AllCustomPersistableTypes>.optDecimal, nil, count: 0)
}

func testGreaterThanAnyRealmValue() {
func validateStringComparisons<Root: Object, T: _Persistable>(
_ name: String, _ lhs: (Query<Root>) -> Query<T>,
_ value: T, letCount: Int = 1, getCount: Int = 1, ltCount: Int = 0, gtCount: Int = 0) where T.PersistedType: _QueryString {
assertQuery(Root.self, "(\(name) > %@)", value, count: gtCount) {
lhs($0) > value
}
assertQuery(Root.self, "(\(name) >= %@)", value, count: getCount) {
lhs($0) >= value
}
assertQuery(Root.self, "(\(name) < %@)", value, count: ltCount) {
lhs($0) < value
}
assertQuery(Root.self, "(\(name) <= %@)", value, count: letCount) {
lhs($0) <= value
}
}

func testStringComparisons() {
validateStringComparisons("stringCol", \Query<ModernAllTypesObject>.stringCol, "Foó")
validateStringComparisons("stringEnumCol", \Query<ModernAllTypesObject>.stringEnumCol, .value2)
validateStringComparisons("string", \Query<AllCustomPersistableTypes>.string, StringWrapper(persistedValue: "Foó"))
validateStringComparisons("optStringCol", \Query<ModernAllTypesObject>.optStringCol, "Foó")
validateStringComparisons("optStringEnumCol", \Query<ModernAllTypesObject>.optStringEnumCol, .value2)
validateStringComparisons("optString", \Query<AllCustomPersistableTypes>.optString, StringWrapper(persistedValue: "Foó"))

validateStringComparisons("optStringCol", \Query<ModernAllTypesObject>.optStringCol, nil, letCount: 0, gtCount: 1)
validateStringComparisons("optStringEnumCol", \Query<ModernAllTypesObject>.optStringEnumCol, nil, letCount: 0, gtCount: 1)
validateStringComparisons("optString", \Query<AllCustomPersistableTypes>.optString, nil, letCount: 0, gtCount: 1)
}

func testGreaterThanNumericAnyRealmValue() {
let object = objects()[0]
setAnyRealmValueCol(with: .int(123), object: object)
assertQuery("(anyCol > %@)", AnyRealmValue.int(123), count: 0) {
Expand Down Expand Up @@ -1777,7 +1807,18 @@ class QueryTests: TestCase, @unchecked Sendable {
}
}

func testLessThanAnyRealmValue() {
func testGreaterThanStringAnyRealmValue() {
let object = objects()[0]
setAnyRealmValueCol(with: .string("FooBar"), object: object)
assertQuery("(anyCol > %@)", AnyRealmValue.string("FooBar"), count: 0) {
$0.anyCol > .string("FooBar")
}
assertQuery("(anyCol >= %@)", AnyRealmValue.string("FooBar"), count: 1) {
$0.anyCol >= .string("FooBar")
}
}

func testLessThanNumericAnyRealmValue() {
let object = objects()[0]
setAnyRealmValueCol(with: .int(123), object: object)
assertQuery("(anyCol < %@)", AnyRealmValue.int(123), count: 0) {
Expand Down Expand Up @@ -1816,6 +1857,17 @@ class QueryTests: TestCase, @unchecked Sendable {
}
}

func testLessThanStringAnyRealmValue() {
let object = objects()[0]
setAnyRealmValueCol(with: .string("FooBar"), object: object)
assertQuery("(anyCol < %@)", AnyRealmValue.string("FooBar"), count: 0) {
$0.anyCol < .string("FooBar")
}
assertQuery("(anyCol <= %@)", AnyRealmValue.string("FooBar"), count: 1) {
$0.anyCol <= .string("FooBar")
}
}

private func validateNumericContains<Root: Object, T: _RealmSchemaDiscoverable & QueryValue & Comparable>(
_ name: String, _ lhs: (Query<Root>) -> Query<T>) {
let values = T.queryValues()
Expand Down Expand Up @@ -2003,6 +2055,57 @@ class QueryTests: TestCase, @unchecked Sendable {
likeStrings.map { (StringWrapper(persistedValue: $0.0), $0.1, $0.2) }, canMatch: true)
}

private func validateStringLexicographicalComparision<Root: Object, T: _Persistable>(
_ name: String, _ lhs: (Query<Root>) -> Query<T>, _ strings: [(T, Int, Int, Int, Int, Bool)], canMatch: Bool) where T.PersistedType: _QueryString {
for (str, gtCount, getCount, ltCount, letCount, caseSensitive) in strings {
assertQuery(Root.self, "(\(name) > %@)", str, count: (canMatch || caseSensitive) ? gtCount : 1) {
lhs($0) > str
}
assertQuery(Root.self, "(\(name) >= %@)", str, count: (canMatch || caseSensitive) ? getCount : 1) {
lhs($0) >= str
}
assertQuery(Root.self, "(\(name) < %@)", str, count: (canMatch || caseSensitive) ? ltCount : 0) {
lhs($0) < str
}
assertQuery(Root.self, "(\(name) <= %@)", str, count: (canMatch || caseSensitive) ? letCount : 0) {
lhs($0) <= str
}
}
}

func testLexicographicalComparision() {
let likeStrings: [(String, Int, Int, Int, Int, Bool)] = [
("Foó", 0, 1, 0, 1, false),
("f*", 0, 0, 1, 1, true),
("*ó", 1, 1, 0, 0, false),
("f?ó", 0, 0, 1, 1, true),
("f*ó", 0, 0, 1, 1, true),
("f??ó", 0, 0, 1, 1, true),
("*o*", 1, 1, 0, 0, false),
("*O*", 1, 1, 0, 0, false),
("?o?", 1, 1, 0, 0, false),
("?O?", 1, 1, 0, 0, false),
("Foô", 0, 0, 1, 1, false),
("Fpó", 0, 0, 1, 1, false),
("Goó", 0, 0, 1, 1, false),
("Foò", 1, 1, 0, 0, false),
("Fnó", 1, 1, 0, 0, false),
("Eoó", 1, 1, 0, 0, false),
]
validateStringLexicographicalComparision("stringCol", \Query<ModernAllTypesObject>.stringCol,
likeStrings.map { ($0.0, $0.1, $0.2, $0.3, $0.4, $0.5) }, canMatch: true)
validateStringLexicographicalComparision("stringEnumCol", \Query<ModernAllTypesObject>.stringEnumCol.rawValue,
likeStrings.map { ($0.0, $0.1, $0.2, $0.3, $0.4, $0.5) }, canMatch: false)
validateStringLexicographicalComparision("string", \Query<AllCustomPersistableTypes>.string,
likeStrings.map { (StringWrapper(persistedValue: $0.0), $0.1, $0.2, $0.3, $0.4, $0.5) }, canMatch: true)
validateStringLexicographicalComparision("optStringCol", \Query<ModernAllTypesObject>.optStringCol,
likeStrings.map { (String?.some($0.0), $0.1, $0.2, $0.3, $0.4, $0.5) }, canMatch: true)
validateStringLexicographicalComparision("optStringEnumCol", \Query<ModernAllTypesObject>.optStringEnumCol.rawValue,
likeStrings.map { (String?.some($0.0), $0.1, $0.2, $0.3, $0.4, $0.5) }, canMatch: false)
validateStringLexicographicalComparision("optString", \Query<AllCustomPersistableTypes>.optString,
likeStrings.map { (StringWrapper(persistedValue: $0.0), $0.1, $0.2, $0.3, $0.4, $0.5) }, canMatch: true)
}

// MARK: - Data

func validateData<Root: Object, T: _Persistable>(_ name: String, _ lhs: (Query<Root>) -> Query<T>,
Expand Down Expand Up @@ -9032,6 +9135,38 @@ class QueryTests: TestCase, @unchecked Sendable {
$0.stringEnumCol != $0.stringEnumCol
}

assertQuery("(stringEnumCol > stringEnumCol)", count: 0) {
$0.stringEnumCol > $0.stringEnumCol
}

assertQuery("(stringCol > stringCol)", count: 0) {
$0.stringCol > $0.stringCol
}

assertQuery("(stringEnumCol >= stringEnumCol)", count: 1) {
$0.stringEnumCol >= $0.stringEnumCol
}

assertQuery("(stringCol >= stringCol)", count: 1) {
$0.stringCol >= $0.stringCol
}

assertQuery("(stringEnumCol < stringEnumCol)", count: 0) {
$0.stringEnumCol < $0.stringEnumCol
}

assertQuery("(stringCol < stringCol)", count: 0) {
$0.stringCol < $0.stringCol
}

assertQuery("(stringEnumCol <= stringEnumCol)", count: 1) {
$0.stringEnumCol <= $0.stringEnumCol
}

assertQuery("(stringCol <= stringCol)", count: 1) {
$0.stringCol <= $0.stringCol
}

assertThrows(assertQuery("", count: 1) {
$0.arrayCol == $0.arrayCol
}, reason: "Comparing two collection columns is not permitted.")
Expand Down
Loading

0 comments on commit 1fc78b2

Please sign in to comment.