import Foundation import CoreData /// Represents a lazy database lookup for a set of objects. open class QuerySet : Equatable { /// Returns the managed object context that will be used to execute any requests. public let context: NSManagedObjectContext /// Returns the name of the entity the request is configured to fetch. public let entityName: String /// Returns the sort descriptors of the receiver. public let sortDescriptors: [NSSortDescriptor] /// Returns the predicate of the receiver. public let predicate: NSPredicate? /// The range of the query, allows you to offset and limit a query public let range: Range? // MARK: Initialization public init(_ context:NSManagedObjectContext, _ entityName:String) { self.context = context self.entityName = entityName self.sortDescriptors = [] self.predicate = nil self.range = nil } /// Create a queryset from another queryset with a different sortdescriptor predicate and range public init(queryset:QuerySet, sortDescriptors:[NSSortDescriptor]?, predicate:NSPredicate?, range: Range?) { self.context = queryset.context self.entityName = queryset.entityName self.sortDescriptors = sortDescriptors ?? [] self.predicate = predicate self.range = range } } /// Methods which return a new queryset extension QuerySet { // MARK: Sorting /// Returns a new QuerySet containing objects ordered by the given sort descriptor. public func orderBy(_ sortDescriptor:NSSortDescriptor) -> QuerySet { return orderBy([sortDescriptor]) } /// Returns a new QuerySet containing objects ordered by the given sort descriptors. public func orderBy(_ sortDescriptors:[NSSortDescriptor]) -> QuerySet { return QuerySet(queryset:self, sortDescriptors:sortDescriptors, predicate:predicate, range:range) } /// Reverses the ordering of the QuerySet public func reverse() -> QuerySet { func reverseSortDescriptor(_ sortDescriptor:NSSortDescriptor) -> NSSortDescriptor { return NSSortDescriptor(key: sortDescriptor.key!, ascending: !sortDescriptor.ascending) } return QuerySet(queryset:self, sortDescriptors:sortDescriptors.map(reverseSortDescriptor), predicate:predicate, range:range) } // MARK: Type-safe Sorting /// Returns a new QuerySet containing objects ordered by the given key path. public func orderBy(_ keyPath: KeyPath, ascending: Bool) -> QuerySet { 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))) -> QuerySet { return orderBy(closure(ModelType.self).sortDescriptor) } /// Returns a new QuerySet containing objects ordered by the given sort descriptors. public func orderBy(_ closure:((ModelType.Type) -> ([SortDescriptor]))) -> QuerySet { return orderBy(closure(ModelType.self).map { $0.sortDescriptor }) } // MARK: Filtering /// Returns a new QuerySet containing objects that match the given predicate. public func filter(_ predicate: Predicate) -> QuerySet { return filter(predicate.predicate) } /// Returns a new QuerySet containing objects that match the given predicate. public func filter(_ predicate:NSPredicate) -> QuerySet { var futurePredicate = predicate if let existingPredicate = self.predicate { futurePredicate = NSCompoundPredicate(type: NSCompoundPredicate.LogicalType.and, subpredicates: [existingPredicate, predicate]) } return QuerySet(queryset:self, sortDescriptors:sortDescriptors, predicate:futurePredicate, range:range) } /// Returns a new QuerySet containing objects that match the given predicates. public func filter(_ predicates:[NSPredicate]) -> QuerySet { let newPredicate = NSCompoundPredicate(type: NSCompoundPredicate.LogicalType.and, subpredicates: predicates) return filter(newPredicate) } /// Returns a new QuerySet containing objects that exclude the given predicate. public func exclude(_ predicate: Predicate) -> QuerySet { return exclude(predicate.predicate) } /// Returns a new QuerySet containing objects that exclude the given predicate. public func exclude(_ predicate:NSPredicate) -> QuerySet { let excludePredicate = NSCompoundPredicate(type: NSCompoundPredicate.LogicalType.not, subpredicates: [predicate]) return filter(excludePredicate) } /// Returns a new QuerySet containing objects that exclude the given predicates. public func exclude(_ predicates:[NSPredicate]) -> QuerySet { let excludePredicate = NSCompoundPredicate(type: NSCompoundPredicate.LogicalType.and, subpredicates: predicates) return exclude(excludePredicate) } // MARK: Type-safe filtering /// Returns a new QuerySet containing objects that match the given predicate. @available(*, deprecated, renamed: "filter(_:)", message: "Replaced by KeyPath filtering https://git.io/Jv2v3") public func filter(_ closure:((ModelType.Type) -> (Predicate))) -> QuerySet { return filter(closure(ModelType.self).predicate) } /// Returns a new QuerySet containing objects that exclude the given predicate. @available(*, deprecated, renamed: "exclude(_:)", message: "Replaced by KeyPath filtering https://git.io/Jv2v3") public func exclude(_ closure:((ModelType.Type) -> (Predicate))) -> QuerySet { return exclude(closure(ModelType.self).predicate) } /// Returns a new QuerySet containing objects that match the given predicatess. @available(*, deprecated, renamed: "filter(_:)", message: "Replaced by KeyPath filtering https://git.io/Jv2v3") public func filter(_ closures:[((ModelType.Type) -> (Predicate))]) -> QuerySet { return filter(closures.map { $0(ModelType.self).predicate }) } /// Returns a new QuerySet containing objects that exclude the given predicates. @available(*, deprecated, renamed: "exclude(_:)", message: "Replaced by KeyPath filtering https://git.io/Jv2v3") public func exclude(_ closures:[((ModelType.Type) -> (Predicate))]) -> QuerySet { return exclude(closures.map { $0(ModelType.self).predicate }) } } /// Functions for evauluating a QuerySet extension QuerySet { // MARK: Subscripting /// Returns the object at the specified index. public func object(_ index: Int) throws -> ModelType? { let request = fetchRequest request.fetchOffset = index request.fetchLimit = 1 let items = try context.fetch(request) return items.first } public subscript(range: ClosedRange) -> QuerySet { get { return self[Range(range)] } } public subscript(range: Range) -> QuerySet { get { var fullRange = range if let currentRange = self.range { fullRange = ((currentRange.lowerBound + range.lowerBound) ..< range.upperBound) } return QuerySet(queryset:self, sortDescriptors:sortDescriptors, predicate:predicate, range:fullRange) } } // Mark: Getters /// Returns the first object in the QuerySet public func first() throws -> ModelType? { return try self.object(0) } /// Returns the last object in the QuerySet public func last() throws -> ModelType? { return try reverse().first() } // MARK: Conversion /// Returns a fetch request equivilent to the QuerySet public var fetchRequest: NSFetchRequest { let request = NSFetchRequest(entityName: entityName) request.predicate = predicate request.sortDescriptors = sortDescriptors if let range = range { request.fetchOffset = range.lowerBound request.fetchLimit = range.upperBound - range.lowerBound } return request } /// Returns an array of all objects matching the QuerySet public func array() throws -> [ModelType] { return try context.fetch(fetchRequest) } // MARK: Count /// Returns the count of objects matching the QuerySet. public func count() throws -> Int { return try context.count(for: fetchRequest) } // MARK: Exists /** Returns true if the QuerySet contains any results, and false if not. :note: Returns nil if the operation could not be completed. */ public func exists() throws -> Bool { let fetchRequest = self.fetchRequest fetchRequest.fetchLimit = 1 let result = try context.count(for: fetchRequest) return result != 0 } // MARK: Deletion /// Deletes all the objects matching the QuerySet. public func delete() throws -> Int { let objects = try array() let deletedCount = objects.count for object in objects { context.delete(object) } return deletedCount } } /// Returns true if the two given querysets are equivilent public func == (lhs: QuerySet, rhs: QuerySet) -> Bool { let context = lhs.context == rhs.context let entityName = lhs.entityName == rhs.entityName let sortDescriptors = lhs.sortDescriptors == rhs.sortDescriptors let predicate = lhs.predicate == rhs.predicate let startIndex = lhs.range?.lowerBound == rhs.range?.lowerBound let endIndex = lhs.range?.upperBound == rhs.range?.upperBound return context && entityName && sortDescriptors && predicate && startIndex && endIndex }