Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,18 @@
### Breaking

* Drops support for Swift 3 and Swift 4. Swift 5 or newer must be used.

### Enhancements

* Added support for ordering by Swift KeyPath, for example:

```swift
queryset.orderBy(\.createdAt, ascending: true)
```

* Added support for filtering and excluding by Swift KeyPath, for example:

```swift
queryset.exclude(\.name == "Kyle")
queryset.filter(\.createdAt > Date())
```
84 changes: 26 additions & 58 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,10 @@ QueryKit, a simple type-safe Core Data query language.

## Usage

To get the most out of QueryKit, and to get full type-safe queries, you may
add extensions for your Core Data models providing properties which describe
your models. You may use [querykit-cli](https://github.com/QueryKit/querykit-cli)
to generate these automatically.

An extension for our a `Person` model might look as follows:

```swift
extension User {
static var name:Attribute<String> { return Attribute("name") }
static var age:Attribute<Int> { return Attribute("age") }
}
```

This provides static properties on our User model which represent each property
on our Core Data model, these may be used to construct predicates and sort
descriptors with compile time safety, without stringly typing them
into your application.

```swift
let namePredicate = Person.name == "Kyle"
let agePredicate = Person.age > 25
let ageSortDescriptor = Person.age.descending()
QuerySet<Person>(context, "Person")
.orderedBy(.name, ascending: true)
.filter(\.age >= 18)
```

### QuerySet
Expand All @@ -40,20 +21,19 @@ results based on the given parameters.
#### Retrieving all objects

```swift
let queryset = Person.queryset(context)
let queryset = QuerySet<Person>(context, "Person")
```

#### Retrieving specific objects with filters

You may filter a QuerySet using the `filter` and `exclude` methods, which
accept a closure passing the model type allowing you to access the
type-safe attributes.
accept a predicate which can be constructed using KeyPath extensions.

The `filter` and `exclude` methods return brand new QuerySet's including your filter.
The `filter` and `exclude` methods return new QuerySet's including your filter.

```swift
queryset.filter { $0.name == "Kyle" }
queryset.exclude { $0.age > 25 }
queryset.filter(\.name == "Kyle")
queryset.exclude(\.age > 25)
```

You may also use standard `NSPredicate` if you want to construct complicated
Expand All @@ -70,14 +50,13 @@ The result of refining a QuerySet is itself a QuerySet, so it’s possible
to chain refinements together. For example:

```swift
queryset.filter { $0.name == "Kyle" }
.exclude { $0.age < 25 }
.filter { $0.isEmployed }
queryset.filter(\.name == "Kyle")
.exclude(\.age < 25)
```

Each time you refine a QuerySet, you get a brand-new QuerySet that is in
no way bound to the previous QuerySet. Each refinement creates a separate
and distinct QuerySet that may be stored, used and reused.
Each time you refine a QuerySet, you get a new QuerySet instance that is in no
way bound to the previous QuerySet. Each refinement creates a separate and
distinct QuerySet that may be stored, used and reused.

#### QuerySets are lazy

Expand All @@ -88,17 +67,15 @@ QuerySet is *evaluated*.
#### Ordering

You may order a QuerySet's results by using the `orderBy` function which
accepts a closure passing the model type, and expects a sort descriptor in
return.
accepts a KeyPath.

```swift
queryset.orderBy { $0.name.ascending() }
queryset.orderBy(\.name, ascending: true)
```

You may also pass in an `NSSortDescriptor` if you would rather.

```swift
queryset.orderBy(Person.name.ascending())
queryset.orderBy(NSSortDescriptor(key: "name", ascending: true))
```

Expand All @@ -108,7 +85,7 @@ Using slicing, a QuerySet's results may be limited to a specified range. For
example, to get the first 5 items in our QuerySet:

```swift
queryset[0..5]
queryset[0...5]
```

**NOTE**: *Remember, QuerySets are lazily evaluated. Slicing doesn’t evaluate the query.*
Expand Down Expand Up @@ -158,35 +135,26 @@ count or an error if the operation failed.
let deleted = try? queryset.delete()
```

#### Attribute

The `Attribute` is a generic structure for creating predicates in a
type-safe manner as shown at the start of the README.

```swift
let name = Attribute<String>("name")
let age = Attribute<Int>("age")
```

##### Operators

QueryKit provides custom operator functions allowing you to create predicates.
QueryKit provides KeyPath extensions providing operator functions allowing you
to create predicates.

```swift
// Name is equal to Kyle
name == "Kyle"
\Person.name == "Kyle"

// Name is either equal to Kyle or Katie
name << ["Kyle", "Katie"]
\.Person.name << ["Kyle", "Katie"]

// Age is equal to 27
age == 27
\.Person.age == 27

// Age is more than or equal to 25
age >= 25
\Person.age >= 25

// Age is within the range 22 to 30.
age << (22...30)
\Person.age << (22...30)
```

The following types of comparisons are supported using Attribute:
Expand All @@ -210,13 +178,13 @@ QueryKit provides the `!`, `&&` and `||` operators for joining multiple predicat

```swift
// Persons name is Kyle or Katie
Person.name == "Kyle" || Person.name == "Katie"
\Person.name == "Kyle" || \Person.name == "Katie"

// Persons age is more than 25 and their name is Kyle
Person.age >= 25 && Person.name == "Kyle"
\Person.age >= 25 && \Person.name == "Kyle"

// Persons name is not Kyle
!(Person.name == "Kyle")
!(\Person.name == "Kyle")
```

## Installation
Expand Down
4 changes: 2 additions & 2 deletions Sources/QueryKit/Attribute.swift
Original file line number Diff line number Diff line change
Expand Up @@ -112,11 +112,11 @@ prefix public func ! (left: Attribute<Bool>) -> NSPredicate {

public extension QuerySet {
func filter(_ attribute:Attribute<Bool>) -> QuerySet<ModelType> {
return filter(attribute == true)
return filter((attribute == true) as NSPredicate)
}

func exclude(_ attribute:Attribute<Bool>) -> QuerySet<ModelType> {
return filter(attribute == false)
return filter((attribute == false) as NSPredicate)
}
}

Expand Down
86 changes: 86 additions & 0 deletions Sources/QueryKit/KeyPath.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import CoreData

// MARK: Predicate

public func == <R : NSManagedObject, V>(lhs: KeyPath<R, V>, rhs: V) -> Predicate<R> {
return Predicate(predicate: lhs == rhs)
}

public func != <R : NSManagedObject, V>(lhs: KeyPath<R, V>, rhs: V) -> Predicate<R> {
return Predicate(predicate: lhs != rhs)
}

public func > <R : NSManagedObject, V>(lhs: KeyPath<R, V>, rhs: V) -> Predicate<R> {
return Predicate(predicate: lhs > rhs)
}

public func >= <R : NSManagedObject, V>(lhs: KeyPath<R, V>, rhs: V) -> Predicate<R> {
return Predicate(predicate: lhs >= rhs)
}

public func < <R : NSManagedObject, V>(lhs: KeyPath<R, V>, rhs: V) -> Predicate<R> {
return Predicate(predicate: lhs < rhs)
}

public func <= <R : NSManagedObject, V>(lhs: KeyPath<R, V>, rhs: V) -> Predicate<R> {
return Predicate(predicate: lhs <= rhs)
}

public func ~= <R : NSManagedObject, V>(lhs: KeyPath<R, V>, rhs: V) -> Predicate<R> {
return Predicate(predicate: lhs ~= rhs)
}

public func << <R : NSManagedObject, V>(lhs: KeyPath<R, V>, rhs: [V]) -> Predicate<R> {
return Predicate(predicate: lhs << rhs)
}

public func << <R : NSManagedObject, V>(lhs: KeyPath<R, V>, rhs: Range<V>) -> Predicate<R> {
return Predicate(predicate: lhs << rhs)
}

// MARK: - NSPredicate

public func == <R : NSManagedObject, V>(lhs: KeyPath<R, V>, rhs: V) -> NSPredicate {
let attribute = Attribute<V>((lhs as AnyKeyPath)._kvcKeyPathString!)
return attribute == rhs
}

public func != <R : NSManagedObject, V>(lhs: KeyPath<R, V>, rhs: V) -> NSPredicate {
let attribute = Attribute<V>((lhs as AnyKeyPath)._kvcKeyPathString!)
return attribute != rhs
}

public func > <R : NSManagedObject, V>(lhs: KeyPath<R, V>, rhs: V) -> NSPredicate {
let attribute = Attribute<V>((lhs as AnyKeyPath)._kvcKeyPathString!)
return attribute > rhs
}

public func >= <R : NSManagedObject, V>(lhs: KeyPath<R, V>, rhs: V) -> NSPredicate {
let attribute = Attribute<V>((lhs as AnyKeyPath)._kvcKeyPathString!)
return attribute >= rhs
}

public func < <R : NSManagedObject, V>(lhs: KeyPath<R, V>, rhs: V) -> NSPredicate {
let attribute = Attribute<V>((lhs as AnyKeyPath)._kvcKeyPathString!)
return attribute < rhs
}

public func <= <R : NSManagedObject, V>(lhs: KeyPath<R, V>, rhs: V) -> NSPredicate {
let attribute = Attribute<V>((lhs as AnyKeyPath)._kvcKeyPathString!)
return attribute <= rhs
}

public func ~= <R : NSManagedObject, V>(lhs: KeyPath<R, V>, rhs: V) -> NSPredicate {
let attribute = Attribute<V>((lhs as AnyKeyPath)._kvcKeyPathString!)
return attribute ~= rhs
}

public func << <R : NSManagedObject, V>(lhs: KeyPath<R, V>, rhs: [V]) -> NSPredicate {
let attribute = Attribute<V>((lhs as AnyKeyPath)._kvcKeyPathString!)
return attribute << rhs
}

public func << <R : NSManagedObject, V>(lhs: KeyPath<R, V>, rhs: Range<V>) -> NSPredicate {
let attribute = Attribute<V>((lhs as AnyKeyPath)._kvcKeyPathString!)
return attribute << rhs
}
15 changes: 15 additions & 0 deletions Sources/QueryKit/QuerySet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ extension QuerySet {

// MARK: Type-safe Sorting

/// Returns a new QuerySet containing objects ordered by the given key path.
public func orderBy<T>(_ keyPath: KeyPath<ModelType, T>, ascending: Bool) -> QuerySet<ModelType> {
return orderBy(NSSortDescriptor(key: (keyPath as AnyKeyPath)._kvcKeyPathString!, ascending: ascending))
}

/// Returns a new QuerySet containing objects ordered by the given sort descriptor.
public func orderBy(_ closure:((ModelType.Type) -> (SortDescriptor<ModelType>))) -> QuerySet<ModelType> {
return orderBy(closure(ModelType.self).sortDescriptor)
Expand All @@ -75,6 +80,11 @@ extension QuerySet {

// MARK: Filtering

/// Returns a new QuerySet containing objects that match the given predicate.
public func filter(_ predicate: Predicate<ModelType>) -> QuerySet<ModelType> {
return filter(predicate.predicate)
}

/// Returns a new QuerySet containing objects that match the given predicate.
public func filter(_ predicate:NSPredicate) -> QuerySet<ModelType> {
var futurePredicate = predicate
Expand All @@ -92,6 +102,11 @@ extension QuerySet {
return filter(newPredicate)
}

/// Returns a new QuerySet containing objects that exclude the given predicate.
public func exclude(_ predicate: Predicate<ModelType>) -> QuerySet<ModelType> {
return exclude(predicate.predicate)
}

/// Returns a new QuerySet containing objects that exclude the given predicate.
public func exclude(_ predicate:NSPredicate) -> QuerySet<ModelType> {
let excludePredicate = NSCompoundPredicate(type: NSCompoundPredicate.LogicalType.not, subpredicates: [predicate])
Expand Down
Loading