From aa32f5e158727e75308852543b463400943b8700 Mon Sep 17 00:00:00 2001 From: John Rommel Estropia Date: Sat, 2 Apr 2016 21:39:05 +0900 Subject: [PATCH] support value querying in Objective C --- CoreStore.xcodeproj/project.pbxproj | 12 + CoreStoreTests/CoreStoreTests.swift | 5 + .../Concrete Clauses/Select.swift | 234 +++++++----- .../NSManagedObjectContext+Querying.swift | 64 +++- .../CSBaseDataTransaction+Querying.swift | 33 +- Sources/ObjectiveC/CSCoreStore+Querying.swift | 33 +- Sources/ObjectiveC/CSDataStack+Querying.swift | 39 +- Sources/ObjectiveC/CSSelect.swift | 350 ++++++++++++++++++ .../NSManagedObjectContext+ObjectiveC.swift | 41 +- 9 files changed, 680 insertions(+), 131 deletions(-) create mode 100644 Sources/ObjectiveC/CSSelect.swift diff --git a/CoreStore.xcodeproj/project.pbxproj b/CoreStore.xcodeproj/project.pbxproj index c493d70..9c779b1 100644 --- a/CoreStore.xcodeproj/project.pbxproj +++ b/CoreStore.xcodeproj/project.pbxproj @@ -300,6 +300,11 @@ B59983491CA54BC100E1A417 /* CSBaseDataTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5519A581CA2008C002BEF78 /* CSBaseDataTransaction.swift */; }; B59AFF411C6593E400C0ABE2 /* NSPersistentStoreCoordinator+Setup.swift in Sources */ = {isa = PBXBuildFile; fileRef = B59AFF401C6593E400C0ABE2 /* NSPersistentStoreCoordinator+Setup.swift */; }; B5A261211B64BFDB006EB6D3 /* MigrationType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A261201B64BFDB006EB6D3 /* MigrationType.swift */; }; + B5A5F2661CAEC50F004AB9AF /* CSSelect.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A5F2651CAEC50F004AB9AF /* CSSelect.swift */; }; + B5A5F2671CAEC50F004AB9AF /* CSSelect.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A5F2651CAEC50F004AB9AF /* CSSelect.swift */; }; + B5A5F2681CAEC50F004AB9AF /* CSSelect.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A5F2651CAEC50F004AB9AF /* CSSelect.swift */; }; + B5A5F2691CAEC50F004AB9AF /* CSSelect.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A5F2651CAEC50F004AB9AF /* CSSelect.swift */; }; + B5A5F26A1CAEC50F004AB9AF /* CSSelect.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A5F2651CAEC50F004AB9AF /* CSSelect.swift */; }; B5AEFAB51C9962AE00AD137F /* CoreStoreBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5AEFAB41C9962AE00AD137F /* CoreStoreBridge.swift */; }; B5AEFAB61C9962AE00AD137F /* CoreStoreBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5AEFAB41C9962AE00AD137F /* CoreStoreBridge.swift */; }; B5AEFAB71C9962AE00AD137F /* CoreStoreBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5AEFAB41C9962AE00AD137F /* CoreStoreBridge.swift */; }; @@ -652,6 +657,7 @@ B56965231B356B820075EE4A /* MigrationResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MigrationResult.swift; sourceTree = ""; }; B59AFF401C6593E400C0ABE2 /* NSPersistentStoreCoordinator+Setup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSPersistentStoreCoordinator+Setup.swift"; sourceTree = ""; }; B5A261201B64BFDB006EB6D3 /* MigrationType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MigrationType.swift; sourceTree = ""; }; + B5A5F2651CAEC50F004AB9AF /* CSSelect.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CSSelect.swift; sourceTree = ""; }; B5AD60CD1C90141E00F2B2E8 /* Package.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = SOURCE_ROOT; }; B5AEFAB41C9962AE00AD137F /* CoreStoreBridge.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreStoreBridge.swift; sourceTree = ""; }; B5BDC91A1C202269008147CD /* Cartfile */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = Cartfile; path = ../Cartfile; sourceTree = ""; }; @@ -1199,6 +1205,7 @@ isa = PBXGroup; children = ( B5ECDBEB1CA6BF2000C7F112 /* CSFrom.swift */, + B5A5F2651CAEC50F004AB9AF /* CSSelect.swift */, B5ECDBFE1CA80CBA00C7F112 /* CSWhere.swift */, B5ECDC041CA8138100C7F112 /* CSOrderBy.swift */, B5ECDC0A1CA8161B00C7F112 /* CSGroupBy.swift */, @@ -1598,6 +1605,7 @@ B5E84F231AFF84860064E85B /* ListMonitor.swift in Sources */, B5E84EF71AFF846E0064E85B /* UnsafeDataTransaction.swift in Sources */, B56964D41B22FFAD0075EE4A /* DataStack+Migration.swift in Sources */, + B5A5F2661CAEC50F004AB9AF /* CSSelect.swift in Sources */, B5ECDBE51CA6BEA300C7F112 /* CSClauseTypes.swift in Sources */, B5519A4A1CA1F4FB002BEF78 /* CSError.swift in Sources */, B5E84EF51AFF846E0064E85B /* BaseDataTransaction.swift in Sources */, @@ -1726,6 +1734,7 @@ 82BA18A91C4BBD3100A0916E /* Into.swift in Sources */, 82BA18D11C4BBD7100A0916E /* NotificationObserver.swift in Sources */, 82BA18BB1C4BBD4A00A0916E /* Where.swift in Sources */, + B5A5F2681CAEC50F004AB9AF /* CSSelect.swift in Sources */, B5ECDBE71CA6BEA300C7F112 /* CSClauseTypes.swift in Sources */, B5519A4B1CA1F4FB002BEF78 /* CSError.swift in Sources */, 82BA18D71C4BBD7100A0916E /* NSManagedObjectModel+Setup.swift in Sources */, @@ -1799,6 +1808,7 @@ B52DD1C21BE1F94600949AFE /* MigrationManager.swift in Sources */, B5ECDC2D1CA81CC700C7F112 /* CSDataStack+Transaction.swift in Sources */, B5D7A5BA1CA3BF8F005C752B /* CSInto.swift in Sources */, + B5A5F26A1CAEC50F004AB9AF /* CSSelect.swift in Sources */, B5FEC1911C9166E700532541 /* NSPersistentStore+Setup.swift in Sources */, B52DD1AB1BE1F93900949AFE /* From.swift in Sources */, B52DD1BF1BE1F94600949AFE /* AssociatedObjects.swift in Sources */, @@ -1964,6 +1974,7 @@ B56321871BD65216006C9394 /* Into.swift in Sources */, B563219A1BD65216006C9394 /* GroupBy.swift in Sources */, B5ECDBE81CA6BEA300C7F112 /* CSClauseTypes.swift in Sources */, + B5A5F2691CAEC50F004AB9AF /* CSSelect.swift in Sources */, B5519A4C1CA1F4FB002BEF78 /* CSError.swift in Sources */, B563219B1BD65216006C9394 /* Tweak.swift in Sources */, B56321B51BD6521C006C9394 /* NSManagedObjectModel+Setup.swift in Sources */, @@ -2070,6 +2081,7 @@ B5D9E30C1CA2C317007A9D52 /* BaseDataTransaction+Querying.swift in Sources */, B5D9E30D1CA2C317007A9D52 /* MigrationManager.swift in Sources */, B5D9E30E1CA2C317007A9D52 /* DataStack+Transaction.swift in Sources */, + B5A5F2671CAEC50F004AB9AF /* CSSelect.swift in Sources */, B5D9E30F1CA2C317007A9D52 /* DataStack.swift in Sources */, B5D9E3101CA2C317007A9D52 /* Functions.swift in Sources */, B5D9E3431CA2C6C4007A9D52 /* GCDBlock.swift in Sources */, diff --git a/CoreStoreTests/CoreStoreTests.swift b/CoreStoreTests/CoreStoreTests.swift index f96d947..1a83ee3 100644 --- a/CoreStoreTests/CoreStoreTests.swift +++ b/CoreStoreTests/CoreStoreTests.swift @@ -88,6 +88,11 @@ class CoreStoreTests: XCTestCase { obj1.testString = "lololol" obj1.testNumber = 42 obj1.testDate = NSDate() + let objID = transaction.queryValue( + From(TestEntity1), + Select(.Attribute("testEntityID")) + ) + print(objID) let count = transaction.queryValue( From(), diff --git a/Sources/Fetching and Querying/Concrete Clauses/Select.swift b/Sources/Fetching and Querying/Concrete Clauses/Select.swift index a357d7d..21b7d18 100644 --- a/Sources/Fetching and Querying/Concrete Clauses/Select.swift +++ b/Sources/Fetching and Querying/Concrete Clauses/Select.swift @@ -32,7 +32,7 @@ import CoreData /** The `SelectResultType` protocol is implemented by return types supported by the `Select` clause. */ -public protocol SelectResultType { } +public protocol SelectResultType {} // MARK: - SelectValueResultType @@ -42,6 +42,8 @@ public protocol SelectResultType { } */ public protocol SelectValueResultType: SelectResultType { + static var attributeType: NSAttributeType { get } + static func fromResultObject(result: AnyObject) -> Self? } @@ -62,7 +64,7 @@ public protocol SelectAttributesResultType: SelectResultType { /** The `SelectTerm` is passed to the `Select` clause to indicate the attributes/aggregate keys to be queried. */ -public enum SelectTerm: StringLiteralConvertible { +public enum SelectTerm: StringLiteralConvertible, Hashable { /** Provides a `SelectTerm` to a `Select` clause for querying an entity attribute. A shorter way to do the same is to assign from the string keypath directly: @@ -218,6 +220,21 @@ public enum SelectTerm: StringLiteralConvertible { } + // MARK: Hashable + + public var hashValue: Int { + + switch self { + + case ._Attribute(let keyPath): + return 0 ^ keyPath.hashValue + + case ._Aggregate(let function, let keyPath, let alias, let nativeType): + return 1 ^ function.hashValue ^ keyPath.hashValue ^ alias.hashValue ^ nativeType.hashValue + } + } + + // MARK: Internal case _Attribute(KeyPath) @@ -225,6 +242,29 @@ public enum SelectTerm: StringLiteralConvertible { } +// MARK: - SelectTerm: Equatable + +@warn_unused_result +public func == (lhs: SelectTerm, rhs: SelectTerm) -> Bool { + + switch (lhs, rhs) { + + case (._Attribute(let keyPath1), ._Attribute(let keyPath2)): + return keyPath1 == keyPath2 + + case (._Aggregate(let function1, let keyPath1, let alias1, let nativeType1), + ._Aggregate(let function2, let keyPath2, let alias2, let nativeType2)): + return function1 == function2 + && keyPath1 == keyPath2 + && alias1 == alias2 + && nativeType1 == nativeType2 + + default: + return false + } +} + + // MARK: - Select /** @@ -267,7 +307,7 @@ public enum SelectTerm: StringLiteralConvertible { - parameter sortDescriptors: a series of `NSSortDescriptor`s */ -public struct Select { +public struct Select: Hashable { /** The `SelectResultType` type for the query's return value @@ -285,93 +325,37 @@ public struct Select { self.selectTerms = [selectTerm] + selectTerms } + /** + Initializes a `Select` clause with a list of `SelectTerm`s + + - parameter selectTerms: a series of `SelectTerm`s + */ + public init(_ selectTerms: [SelectTerm]) { + + self.selectTerms = selectTerms + } + + + // MARK: Hashable + + public var hashValue: Int { + + return self.selectTerms.map { $0.hashValue }.reduce(0, combine: ^) + } + // MARK: Internal - internal func applyToFetchRequest(fetchRequest: NSFetchRequest) { - - if fetchRequest.propertiesToFetch != nil { - - CoreStore.log( - .Warning, - message: "An existing \"propertiesToFetch\" for the \(typeName(NSFetchRequest)) was overwritten by \(typeName(self)) query clause." - ) - } - - fetchRequest.includesPendingChanges = false - fetchRequest.resultType = .DictionaryResultType - - let entityDescription = fetchRequest.entity! - let propertiesByName = entityDescription.propertiesByName - let attributesByName = entityDescription.attributesByName - - var propertiesToFetch = [AnyObject]() - for term in self.selectTerms { - - switch term { - - case ._Attribute(let keyPath): - if let propertyDescription = propertiesByName[keyPath] { - - propertiesToFetch.append(propertyDescription) - } - else { - - CoreStore.log( - .Warning, - message: "The property \"\(keyPath)\" does not exist in entity \(typeName(entityDescription.managedObjectClassName)) and will be ignored by \(typeName(self)) query clause." - ) - } - - case ._Aggregate(let function, let keyPath, let alias, let nativeType): - if let attributeDescription = attributesByName[keyPath] { - - let expressionDescription = NSExpressionDescription() - expressionDescription.name = alias - if nativeType == .UndefinedAttributeType { - - expressionDescription.expressionResultType = attributeDescription.attributeType - } - else { - - expressionDescription.expressionResultType = nativeType - } - expressionDescription.expression = NSExpression( - forFunction: function, - arguments: [NSExpression(forKeyPath: keyPath)] - ) - - propertiesToFetch.append(expressionDescription) - } - else { - - CoreStore.log( - .Warning, - message: "The attribute \"\(keyPath)\" does not exist in entity \(typeName(entityDescription.managedObjectClassName)) and will be ignored by \(typeName(self)) query clause." - ) - } - } - } - - fetchRequest.propertiesToFetch = propertiesToFetch - } + internal let selectTerms: [SelectTerm] +} + + +// MARK: - Select: Equatable + +@warn_unused_result +public func == (lhs: Select, rhs: Select) -> Bool { - internal func keyPathForFirstSelectTerm() -> KeyPath { - - switch self.selectTerms.first! { - - case ._Attribute(let keyPath): - return keyPath - - case ._Aggregate(_, _, let alias, _): - return alias - } - } - - - // MARK: Private - - private let selectTerms: [SelectTerm] + return lhs.selectTerms == rhs.selectTerms } @@ -666,3 +650,81 @@ extension NSDictionary: SelectAttributesResultType { return result as! [[NSString: AnyObject]] } } + + +// MARK: - Internal + +internal extension CollectionType where Generator.Element == SelectTerm { + + internal func applyToFetchRequest(fetchRequest: NSFetchRequest, owner: T) { + + fetchRequest.includesPendingChanges = false + fetchRequest.resultType = .DictionaryResultType + + let entityDescription = fetchRequest.entity! + let propertiesByName = entityDescription.propertiesByName + let attributesByName = entityDescription.attributesByName + + var propertiesToFetch = [AnyObject]() + for term in self { + + switch term { + + case ._Attribute(let keyPath): + if let propertyDescription = propertiesByName[keyPath] { + + propertiesToFetch.append(propertyDescription) + } + else { + + CoreStore.log( + .Warning, + message: "The property \"\(keyPath)\" does not exist in entity \(typeName(entityDescription.managedObjectClassName)) and will be ignored by \(typeName(owner)) query clause." + ) + } + + case ._Aggregate(let function, let keyPath, let alias, let nativeType): + if let attributeDescription = attributesByName[keyPath] { + + let expressionDescription = NSExpressionDescription() + expressionDescription.name = alias + if nativeType == .UndefinedAttributeType { + + expressionDescription.expressionResultType = attributeDescription.attributeType + } + else { + + expressionDescription.expressionResultType = nativeType + } + expressionDescription.expression = NSExpression( + forFunction: function, + arguments: [NSExpression(forKeyPath: keyPath)] + ) + + propertiesToFetch.append(expressionDescription) + } + else { + + CoreStore.log( + .Warning, + message: "The attribute \"\(keyPath)\" does not exist in entity \(typeName(entityDescription.managedObjectClassName)) and will be ignored by \(typeName(owner)) query clause." + ) + } + } + } + + fetchRequest.propertiesToFetch = propertiesToFetch + } + + internal func keyPathForFirstSelectTerm() -> KeyPath { + + switch self.first! { + + case ._Attribute(let keyPath): + return keyPath + + case ._Aggregate(_, _, let alias, _): + return alias + } + } +} diff --git a/Sources/Internal/NSManagedObjectContext+Querying.swift b/Sources/Internal/NSManagedObjectContext+Querying.swift index d95fb32..a069ab1 100644 --- a/Sources/Internal/NSManagedObjectContext+Querying.swift +++ b/Sources/Internal/NSManagedObjectContext+Querying.swift @@ -387,12 +387,15 @@ internal extension NSManagedObjectContext { fetchRequest.fetchLimit = 0 - selectClause.applyToFetchRequest(fetchRequest) + let selectTerms = selectClause.selectTerms + selectTerms.applyToFetchRequest(fetchRequest, owner: selectClause) + queryClauses.forEach { $0.applyToFetchRequest(fetchRequest) } - for clause in queryClauses { - - clause.applyToFetchRequest(fetchRequest) - } + return self.queryValue(selectTerms, fetchRequest: fetchRequest) + } + + @nonobjc + internal func queryValue(selectTerms: [SelectTerm], fetchRequest: NSFetchRequest) -> U? { var fetchResults: [AnyObject]? var fetchError: ErrorType? @@ -410,9 +413,42 @@ internal extension NSManagedObjectContext { if let fetchResults = fetchResults { if let rawResult = fetchResults.first as? NSDictionary, - let rawObject: AnyObject = rawResult[selectClause.keyPathForFirstSelectTerm()] { - - return Select.ReturnType.fromResultObject(rawObject) + let rawObject: AnyObject = rawResult[selectTerms.keyPathForFirstSelectTerm()] { + + return Select.ReturnType.fromResultObject(rawObject) + } + return nil + } + + CoreStore.log( + CoreStoreError(fetchError), + "Failed executing fetch request." + ) + return nil + } + + @nonobjc + internal func queryValue(selectTerms: [SelectTerm], fetchRequest: NSFetchRequest) -> AnyObject? { + + var fetchResults: [AnyObject]? + var fetchError: ErrorType? + self.performBlockAndWait { + + do { + + fetchResults = try self.executeFetchRequest(fetchRequest) + } + catch { + + fetchError = error + } + } + if let fetchResults = fetchResults { + + if let rawResult = fetchResults.first as? NSDictionary, + let rawObject: AnyObject = rawResult[selectTerms.keyPathForFirstSelectTerm()] { + + return rawObject } return nil } @@ -441,12 +477,14 @@ internal extension NSManagedObjectContext { fetchRequest.fetchLimit = 0 - selectClause.applyToFetchRequest(fetchRequest) + selectClause.selectTerms.applyToFetchRequest(fetchRequest, owner: selectClause) + queryClauses.forEach { $0.applyToFetchRequest(fetchRequest) } - for clause in queryClauses { - - clause.applyToFetchRequest(fetchRequest) - } + return self.queryAttributes(fetchRequest) + } + + @nonobjc + internal func queryAttributes(fetchRequest: NSFetchRequest) -> [[NSString: AnyObject]]? { var fetchResults: [AnyObject]? var fetchError: ErrorType? diff --git a/Sources/ObjectiveC/CSBaseDataTransaction+Querying.swift b/Sources/ObjectiveC/CSBaseDataTransaction+Querying.swift index 2db4710..233ce62 100644 --- a/Sources/ObjectiveC/CSBaseDataTransaction+Querying.swift +++ b/Sources/ObjectiveC/CSBaseDataTransaction+Querying.swift @@ -170,37 +170,44 @@ public extension CSBaseDataTransaction { } /** - Fetches the `NSManagedObjectID` for all `NSManagedObject`s that satisfy the specified `CSFetchClause`s. Accepts `CSWhere`, `CSOrderBy`, and `CSTweak` clauses. + Queries aggregate values as specified by the `CSQueryClause`s. Requires at least a `CSSelect` clause, and optional `CSWhere`, `CSOrderBy`, `CSGroupBy`, and `CSTweak` clauses. + + A "query" differs from a "fetch" in that it only retrieves values already stored in the persistent store. As such, values from unsaved transactions or contexts will not be incorporated in the query result. - parameter from: a `CSFrom` clause indicating the entity type - - parameter fetchClauses: a series of `FetchClause` instances for the fetch request. Accepts `CSWhere`, `CSOrderBy`, and `CSTweak` clauses. - - returns: the `NSManagedObjectID` for all `NSManagedObject`s that satisfy the specified `CSFetchClause`s + - parameter selectClause: a `CSSelect` clause indicating the properties to fetch, and with the generic type indicating the return type. + - parameter queryClauses: a series of `CSQueryClause` instances for the query request. Accepts `CSWhere`, `CSOrderBy`, `CSGroupBy`, and `CSTweak` clauses. + - returns: the result of the the query. The type of the return value is specified by the generic type of the `CSSelect` parameter. */ @objc @warn_unused_result - public func fetchObjectIDsFrom(from: CSFrom, fetchClauses: [CSFetchClause]) -> [NSManagedObjectID]? { + public func queryValueFrom(from: CSFrom, selectClause: CSSelect, queryClauses: [CSQueryClause]) -> AnyObject? { CoreStore.assert( self.bridgeToSwift.isRunningInAllowedQueue(), - "Attempted to fetch from a \(typeName(self)) outside its designated queue." + "Attempted to query from a \(typeName(self)) outside its designated queue." ) - return self.bridgeToSwift.context.fetchObjectIDs(from, fetchClauses) + return self.bridgeToSwift.context.queryValue(from, selectClause, queryClauses) } /** - Deletes all `NSManagedObject`s that satisfy the specified `DeleteClause`s. Accepts `Where`, `OrderBy`, and `Tweak` clauses. + Queries a dictionary of attribute values as specified by the `CSQueryClause`s. Requires at least a `CSSelect` clause, and optional `CSWhere`, `CSOrderBy`, `CSGroupBy`, and `CSTweak` clauses. - - parameter from: a `From` clause indicating the entity type - - parameter deleteClauses: a series of `DeleteClause` instances for the delete request. Accepts `Where`, `OrderBy`, and `Tweak` clauses. - - returns: the number of `NSManagedObject`s deleted + A "query" differs from a "fetch" in that it only retrieves values already stored in the persistent store. As such, values from unsaved transactions or contexts will not be incorporated in the query result. + + - parameter from: a `CSFrom` clause indicating the entity type + - parameter selectClause: a `CSSelect` clause indicating the properties to fetch, and with the generic type indicating the return type. + - parameter queryClauses: a series of `CSQueryClause` instances for the query request. Accepts `CSWhere`, `CSOrderBy`, `CSGroupBy`, and `CSTweak` clauses. + - returns: the result of the the query. The type of the return value is specified by the generic type of the `CSSelect` parameter. */ @objc - public func deleteAllFrom(from: CSFrom, deleteClauses: [CSDeleteClause]) -> NSNumber? { + @warn_unused_result + public func queryAttributesFrom(from: CSFrom, selectClause: CSSelect, queryClauses: [CSQueryClause]) -> [[NSString: AnyObject]]? { CoreStore.assert( self.bridgeToSwift.isRunningInAllowedQueue(), - "Attempted to delete from a \(typeName(self)) outside its designated queue." + "Attempted to query from a \(typeName(self)) outside its designated queue." ) - return self.bridgeToSwift.context.deleteAll(from, deleteClauses) + return self.bridgeToSwift.context.queryAttributes(from, selectClause, queryClauses) } } diff --git a/Sources/ObjectiveC/CSCoreStore+Querying.swift b/Sources/ObjectiveC/CSCoreStore+Querying.swift index cce966d..2270a7c 100644 --- a/Sources/ObjectiveC/CSCoreStore+Querying.swift +++ b/Sources/ObjectiveC/CSCoreStore+Querying.swift @@ -154,15 +154,36 @@ public extension CSCoreStore { } /** - Using the `defaultStack`, deletes all `NSManagedObject`s that satisfy the specified `DeleteClause`s. Accepts `Where`, `OrderBy`, and `Tweak` clauses. + Using the `defaultStack`, queries aggregate values as specified by the `CSQueryClause`s. Requires at least a `CSSelect` clause, and optional `CSWhere`, `CSOrderBy`, `CSGroupBy`, and `CSTweak` clauses. - - parameter from: a `From` clause indicating the entity type - - parameter deleteClauses: a series of `DeleteClause` instances for the delete request. Accepts `Where`, `OrderBy`, and `Tweak` clauses. - - returns: the number of `NSManagedObject`s deleted + A "query" differs from a "fetch" in that it only retrieves values already stored in the persistent store. As such, values from unsaved transactions or contexts will not be incorporated in the query result. + + - parameter from: a `CSFrom` clause indicating the entity type + - parameter selectClause: a `CSSelect` clause indicating the properties to fetch, and with the generic type indicating the return type. + - parameter queryClauses: a series of `CSQueryClause` instances for the query request. Accepts `CSWhere`, `CSOrderBy`, `CSGroupBy`, and `CSTweak` clauses. + - returns: the result of the the query. The type of the return value is specified by the generic type of the `CSSelect` parameter. */ @objc - public static func deleteAllFrom(from: CSFrom, deleteClauses: [CSDeleteClause]) -> NSNumber? { + @warn_unused_result + public static func queryValueFrom(from: CSFrom, selectClause: CSSelect, queryClauses: [CSQueryClause]) -> AnyObject? { - return self.defaultStack.deleteAllFrom(from, deleteClauses: deleteClauses) + return self.defaultStack.queryValueFrom(from, selectClause: selectClause, queryClauses: queryClauses) + } + + /** + Using the `defaultStack`, queries a dictionary of attribute values as specified by the `CSQueryClause`s. Requires at least a `CSSelect` clause, and optional `CSWhere`, `CSOrderBy`, `CSGroupBy`, and `CSTweak` clauses. + + A "query" differs from a "fetch" in that it only retrieves values already stored in the persistent store. As such, values from unsaved transactions or contexts will not be incorporated in the query result. + + - parameter from: a `CSFrom` clause indicating the entity type + - parameter selectClause: a `CSSelect` clause indicating the properties to fetch, and with the generic type indicating the return type. + - parameter queryClauses: a series of `CSQueryClause` instances for the query request. Accepts `CSWhere`, `CSOrderBy`, `CSGroupBy`, and `CSTweak` clauses. + - returns: the result of the the query. The type of the return value is specified by the generic type of the `CSSelect` parameter. + */ + @objc + @warn_unused_result + public static func queryAttributesFrom(from: CSFrom, selectClause: CSSelect, queryClauses: [CSQueryClause]) -> [[NSString: AnyObject]]? { + + return self.defaultStack.queryAttributesFrom(from, selectClause: selectClause, queryClauses: queryClauses) } } diff --git a/Sources/ObjectiveC/CSDataStack+Querying.swift b/Sources/ObjectiveC/CSDataStack+Querying.swift index 74f7fac..3d68ad4 100644 --- a/Sources/ObjectiveC/CSDataStack+Querying.swift +++ b/Sources/ObjectiveC/CSDataStack+Querying.swift @@ -188,19 +188,44 @@ public extension CSDataStack { } /** - Deletes all `NSManagedObject`s that satisfy the specified `DeleteClause`s. Accepts `Where`, `OrderBy`, and `Tweak` clauses. + Queries aggregate values as specified by the `CSQueryClause`s. Requires at least a `CSSelect` clause, and optional `CSWhere`, `CSOrderBy`, `CSGroupBy`, and `CSTweak` clauses. - - parameter from: a `From` clause indicating the entity type - - parameter deleteClauses: a series of `DeleteClause` instances for the delete request. Accepts `Where`, `OrderBy`, and `Tweak` clauses. - - returns: the number of `NSManagedObject`s deleted + A "query" differs from a "fetch" in that it only retrieves values already stored in the persistent store. As such, values from unsaved transactions or contexts will not be incorporated in the query result. + + - parameter from: a `CSFrom` clause indicating the entity type + - parameter selectClause: a `CSSelect` clause indicating the properties to fetch, and with the generic type indicating the return type. + - parameter queryClauses: a series of `CSQueryClause` instances for the query request. Accepts `CSWhere`, `CSOrderBy`, `CSGroupBy`, and `CSTweak` clauses. + - returns: the result of the the query. The type of the return value is specified by the generic type of the `CSSelect` parameter. */ @objc - public func deleteAllFrom(from: CSFrom, deleteClauses: [CSDeleteClause]) -> NSNumber? { + @warn_unused_result + public func queryValueFrom(from: CSFrom, selectClause: CSSelect, queryClauses: [CSQueryClause]) -> AnyObject? { CoreStore.assert( NSThread.isMainThread(), - "Attempted to delete from a \(typeName(self)) outside the main thread." + "Attempted to query from a \(typeName(self)) outside the main thread." ) - return self.bridgeToSwift.mainContext.deleteAll(from, deleteClauses) + return self.bridgeToSwift.mainContext.queryValue(from, selectClause, queryClauses) + } + + /** + Queries a dictionary of attribute values as specified by the `CSQueryClause`s. Requires at least a `CSSelect` clause, and optional `CSWhere`, `CSOrderBy`, `CSGroupBy`, and `CSTweak` clauses. + + A "query" differs from a "fetch" in that it only retrieves values already stored in the persistent store. As such, values from unsaved transactions or contexts will not be incorporated in the query result. + + - parameter from: a `CSFrom` clause indicating the entity type + - parameter selectClause: a `CSSelect` clause indicating the properties to fetch, and with the generic type indicating the return type. + - parameter queryClauses: a series of `CSQueryClause` instances for the query request. Accepts `CSWhere`, `CSOrderBy`, `CSGroupBy`, and `CSTweak` clauses. + - returns: the result of the the query. The type of the return value is specified by the generic type of the `CSSelect` parameter. + */ + @objc + @warn_unused_result + public func queryAttributesFrom(from: CSFrom, selectClause: CSSelect, queryClauses: [CSQueryClause]) -> [[NSString: AnyObject]]? { + + CoreStore.assert( + NSThread.isMainThread(), + "Attempted to query from a \(typeName(self)) outside the main thread." + ) + return self.bridgeToSwift.mainContext.queryAttributes(from, selectClause, queryClauses) } } diff --git a/Sources/ObjectiveC/CSSelect.swift b/Sources/ObjectiveC/CSSelect.swift new file mode 100644 index 0000000..ad4522b --- /dev/null +++ b/Sources/ObjectiveC/CSSelect.swift @@ -0,0 +1,350 @@ +// +// CSSelect.swift +// CoreStore +// +// Copyright © 2016 John Rommel Estropia +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation +import CoreData + + +// MARK: - CSSelectTerm + +/** + The `CSSelectTerm` serves as the Objective-C bridging type for `SelectTerm`. + */ +@objc +public final class CSSelectTerm: NSObject, CoreStoreObjectiveCType { + + /** + Provides a `CSSelectTerm` to a `CSSelect` clause for querying an entity attribute. + ``` + NSString *fullName = [CSCoreStore + queryValueFrom:[CSFrom entityClass:[MyPersonEntity class]] + select:[CSSelect stringForTerm:[CSSelectTerm attribute:@"fullName"]] + fetchClauses:@[[CSWhere keyPath:@"employeeID" isEqualTo: @1111]]]; + ``` + - parameter keyPath: the attribute name + - returns: a `CSSelectTerm` to a `CSSelect` clause for querying an entity attribute + */ + public static func attribute(keyPath: KeyPath) -> CSSelectTerm { + + return self.init(.Attribute(keyPath)) + } + + /** + Provides a `CSSelectTerm` to a `CSSelect` clause for querying the average value of an attribute. + ``` + NSNumber *averageAge = [CSCoreStore + queryValueFrom:[CSFrom entityClass:[MyPersonEntity class]] + select:[CSSelect numberForTerm:[CSSelectTerm average:@"age" as:nil]]]; + ``` + - parameter keyPath: the attribute name + - parameter `as`: the dictionary key to use to access the result. Ignored when the query return value is not an `NSDictionary`. If `nil`, the default key "average()" is used + - returns: a `CSSelectTerm` to a `CSSelect` clause for querying the average value of an attribute + */ + public static func average(keyPath: KeyPath, `as` alias: KeyPath?) -> CSSelectTerm { + + return self.init(.Average(keyPath, As: alias)) + } + + /** + Provides a `CSSelectTerm` to a `CSSelect` clause for a count query. + ``` + NSNumber *numberOfEmployees = [CSCoreStore + queryValueFrom:[CSFrom entityClass:[MyPersonEntity class]] + select:[CSSelect numberForTerm:[CSSelectTerm count:@"employeeID" as:nil]]]; + ``` + - parameter keyPath: the attribute name + - parameter alias: the dictionary key to use to access the result. Ignored when the query return value is not an `NSDictionary`. If `nil`, the default key "count()" is used + - returns: a `SelectTerm` to a `Select` clause for a count query + */ + public static func count(keyPath: KeyPath, `as` alias: KeyPath?) -> CSSelectTerm { + + return self.init(.Count(keyPath, As: alias)) + } + + /** + Provides a `CSSelectTerm` to a `CSSelect` clause for querying the maximum value for an attribute. + ``` + NSNumber *maximumAge = [CSCoreStore + queryValueFrom:[CSFrom entityClass:[MyPersonEntity class]] + select:[CSSelect numberForTerm:[CSSelectTerm maximum:@"age" as:nil]]]; + ``` + - parameter keyPath: the attribute name + - parameter alias: the dictionary key to use to access the result. Ignored when the query return value is not an `NSDictionary`. If `nil`, the default key "max()" is used + - returns: a `CSSelectTerm` to a `CSSelect` clause for querying the maximum value for an attribute + */ + public static func maximum(keyPath: KeyPath, `as` alias: KeyPath?) -> CSSelectTerm { + + return self.init(.Maximum(keyPath, As: alias)) + } + + /** + Provides a `CSSelectTerm` to a `CSSelect` clause for querying the minimum value for an attribute. + ``` + NSNumber *minimumAge = [CSCoreStore + queryValueFrom:[CSFrom entityClass:[MyPersonEntity class]] + select:[CSSelect numberForTerm:[CSSelectTerm minimum:@"age" as:nil]]]; + ``` + - parameter keyPath: the attribute name + - parameter alias: the dictionary key to use to access the result. Ignored when the query return value is not an `NSDictionary`. If `nil`, the default key "min()" is used + - returns: a `CSSelectTerm` to a `CSSelect` clause for querying the minimum value for an attribute + */ + public static func minimum(keyPath: KeyPath, `as` alias: KeyPath?) -> CSSelectTerm { + + return self.init(.Minimum(keyPath, As: alias)) + } + + /** + Provides a `CSSelectTerm` to a `CSSelect` clause for querying the sum value for an attribute. + ``` + NSNumber *totalAge = [CSCoreStore + queryValueFrom:[CSFrom entityClass:[MyPersonEntity class]] + select:[CSSelect numberForTerm:[CSSelectTerm sum:@"age" as:nil]]]; + ``` + - parameter keyPath: the attribute name + - parameter alias: the dictionary key to use to access the result. Ignored when the query return value is not an `NSDictionary`. If `nil`, the default key "sum()" is used + - returns: a `CSSelectTerm` to a `CSSelect` clause for querying the sum value for an attribute + */ + public static func sum(keyPath: KeyPath, `as` alias: KeyPath?) -> CSSelectTerm { + + return self.init(.Sum(keyPath, As: alias)) + } + + + + // MARK: NSObject + + public override var hash: Int { + + return self.bridgeToSwift.hashValue + } + + public override func isEqual(object: AnyObject?) -> Bool { + + guard let object = object as? CSSelectTerm else { + + return false + } + return self.bridgeToSwift == object.bridgeToSwift + } + + + // MARK: CoreStoreObjectiveCType + + public let bridgeToSwift: SelectTerm + + public init(_ swiftValue: SelectTerm) { + + self.bridgeToSwift = swiftValue + super.init() + } +} + + +// MARK: - SelectTerm + +extension SelectTerm: CoreStoreSwiftType { + + // MARK: CoreStoreSwiftType + + public typealias ObjectiveCType = CSSelectTerm +} + + +// MARK: - CSSelect + +/** + The `CSSelect` serves as the Objective-C bridging type for `Select`. + */ +@objc +public final class CSSelect: NSObject { + + /** + Creates a `CSSelect` clause for querying `NSNumber` values. + ``` + NSNumber *maximumAge = [CSCoreStore + queryValueFrom:[CSFrom entityClass:[MyPersonEntity class]] + select:[CSSelect numberForTerm:[CSSelectTerm maximum:@"age" as:nil]]]; + ``` + - parameter term: the `CSSelectTerm` specifying the attribute/aggregate value to query + - returns: a `CSSelect` clause for querying an entity attribute + */ + public static func numberForTerm(term: CSSelectTerm) -> CSSelect { + + return self.init(Select(term.bridgeToSwift)) + } + + /** + Creates a `CSSelect` clause for querying `NSDecimalNumber` values. + ``` + NSNumber *averageAge = [CSCoreStore + queryValueFrom:[CSFrom entityClass:[MyPersonEntity class]] + select:[CSSelect decimalNumberForTerm:[CSSelectTerm average:@"age" as:nil]]]; + ``` + - parameter term: the `CSSelectTerm` specifying the attribute/aggregate value to query + - returns: a `CSSelect` clause for querying an entity attribute + */ + public static func decimalNumberForTerm(term: CSSelectTerm) -> CSSelect { + + return self.init(Select(term.bridgeToSwift)) + } + + /** + Creates a `CSSelect` clause for querying `NSString` values. + ``` + NSString *fullName = [CSCoreStore + queryValueFrom:[CSFrom entityClass:[MyPersonEntity class]] + select:[CSSelect stringForTerm:[CSSelectTerm attribute:@"fullName"]] + fetchClauses:@[[CSWhere keyPath:@"employeeID" isEqualTo: @1111]]]; + ``` + - parameter term: the `CSSelectTerm` specifying the attribute/aggregate value to query + - returns: a `CSSelect` clause for querying an entity attribute + */ + public static func stringForTerm(term: CSSelectTerm) -> CSSelect { + + return self.init(Select(term.bridgeToSwift)) + } + + /** + Creates a `CSSelect` clause for querying `NSDate` values. + ``` + NSDate *lastUpdatedDate = [CSCoreStore + queryValueFrom:[CSFrom entityClass:[MyPersonEntity class]] + select:[CSSelect dateForTerm:[CSSelectTerm maximum:@"updatedDate" as:nil]]]; + ``` + - parameter term: the `CSSelectTerm` specifying the attribute/aggregate value to query + - returns: a `CSSelect` clause for querying an entity attribute + */ + public static func dateForTerm(term: CSSelectTerm) -> CSSelect { + + return self.init(Select(term.bridgeToSwift)) + } + + /** + Creates a `CSSelect` clause for querying `NSData` values. + ``` + NSData *imageData = [CSCoreStore + queryValueFrom:[CSFrom entityClass:[MyPersonEntity class]] + select:[CSSelect dataForTerm:[CSSelectTerm attribute:@"imageData" as:nil]] + fetchClauses:@[[CSWhere keyPath:@"employeeID" isEqualTo: @1111]]]; + ``` + - parameter term: the `CSSelectTerm` specifying the attribute/aggregate value to query + - returns: a `CSSelect` clause for querying an entity attribute + */ + public static func dataForTerm(term: CSSelectTerm) -> CSSelect { + + return self.init(Select(term.bridgeToSwift)) + } + + /** + Creates a `CSSelect` clause for querying `NSManagedObjectID` values. + ``` + NSManagedObjectID *objectIDForOldest = [CSCoreStore + queryValueFrom:[CSFrom entityClass:[MyPersonEntity class]] + select:[CSSelect managedObjectIDForTerm:[CSSelectTerm attribute:@"age" as:nil]] + fetchClauses:@[[CSWhere keyPath:@"employeeID" isEqualTo: @1111]]]; + ``` + - parameter term: the `CSSelectTerm` specifying the attribute/aggregate value to query + - returns: a `CSSelect` clause for querying an entity attribute + */ + public static func managedObjectIDForTerm(term: CSSelectTerm) -> CSSelect { + + return self.init(Select(term.bridgeToSwift)) + } + + /** + Creates a `CSSelect` clause for querying `NSDictionary` of an entity's attribute keys and values. + ``` + NSDictionary *keyValues = [CSCoreStore + queryValueFrom:[CSFrom entityClass:[MyPersonEntity class]] + select:[CSSelect dictionaryForTerm:[CSSelectTerm maximum:@"age" as:nil]]]; + ``` + - parameter term: the `CSSelectTerm` specifying the attribute/aggregate value to query + - returns: a `CSSelect` clause for querying an entity attribute + */ + public static func dictionaryForTerm(term: CSSelectTerm) -> CSSelect { + + return self.init(Select(term.bridgeToSwift)) + } + + public static func dictionaryForTerms(terms: [CSSelectTerm]) -> CSSelect { + + return self.init(Select(terms.map { $0.bridgeToSwift })) + } + + + // MARK: NSObject + + public override var hash: Int { + + return self.attributeType.hashValue + ^ self.selectTerms.map { $0.hashValue }.reduce(0, combine: ^) + } + + public override func isEqual(object: AnyObject?) -> Bool { + + guard let object = object as? CSSelect else { + + return false + } + return self.attributeType == object.attributeType + && self.selectTerms == object.selectTerms + } + + + // MARK: CoreStoreObjectiveCType + + public init(_ swiftValue: Select) { + + self.attributeType = T.attributeType + self.selectTerms = swiftValue.selectTerms + super.init() + } + + public init(_ swiftValue: Select) { + + self.attributeType = .UndefinedAttributeType + self.selectTerms = swiftValue.selectTerms + super.init() + } + + + // MARK: Internal + + internal let attributeType: NSAttributeType + internal let selectTerms: [SelectTerm] +} + + +// MARK: - Select + +extension Select: CoreStoreSwiftType { + + // MARK: CoreStoreSwiftType + + public var bridgeToObjectiveC: CSSelect { + + return CSSelect(self) + } +} diff --git a/Sources/ObjectiveC/NSManagedObjectContext+ObjectiveC.swift b/Sources/ObjectiveC/NSManagedObjectContext+ObjectiveC.swift index ce0a48a..d977835 100644 --- a/Sources/ObjectiveC/NSManagedObjectContext+ObjectiveC.swift +++ b/Sources/ObjectiveC/NSManagedObjectContext+ObjectiveC.swift @@ -36,7 +36,7 @@ internal extension NSManagedObjectContext { @nonobjc internal func fetchOne(from: CSFrom, _ fetchClauses: [CSFetchClause]) -> NSManagedObject? { - let fetchRequest = NSFetchRequest() + let fetchRequest = CoreStoreFetchRequest() from.bridgeToSwift.applyToFetchRequest(fetchRequest, context: self) fetchRequest.fetchLimit = 1 @@ -49,7 +49,7 @@ internal extension NSManagedObjectContext { @nonobjc internal func fetchAll(from: CSFrom, _ fetchClauses: [CSFetchClause]) -> [NSManagedObject]? { - let fetchRequest = NSFetchRequest() + let fetchRequest = CoreStoreFetchRequest() from.bridgeToSwift.applyToFetchRequest(fetchRequest, context: self) fetchRequest.fetchLimit = 0 @@ -62,7 +62,7 @@ internal extension NSManagedObjectContext { @nonobjc internal func fetchCount(from: CSFrom, _ fetchClauses: [CSFetchClause]) -> Int? { - let fetchRequest = NSFetchRequest() + let fetchRequest = CoreStoreFetchRequest() from.bridgeToSwift.applyToFetchRequest(fetchRequest, context: self) fetchClauses.forEach { $0.applyToFetchRequest(fetchRequest) } @@ -72,7 +72,7 @@ internal extension NSManagedObjectContext { @nonobjc internal func fetchObjectID(from: CSFrom, _ fetchClauses: [CSFetchClause]) -> NSManagedObjectID? { - let fetchRequest = NSFetchRequest() + let fetchRequest = CoreStoreFetchRequest() from.bridgeToSwift.applyToFetchRequest(fetchRequest, context: self) fetchRequest.fetchLimit = 1 @@ -85,7 +85,7 @@ internal extension NSManagedObjectContext { @nonobjc internal func fetchObjectIDs(from: CSFrom, _ fetchClauses: [CSFetchClause]) -> [NSManagedObjectID]? { - let fetchRequest = NSFetchRequest() + let fetchRequest = CoreStoreFetchRequest() from.bridgeToSwift.applyToFetchRequest(fetchRequest, context: self) fetchRequest.fetchLimit = 0 @@ -98,7 +98,7 @@ internal extension NSManagedObjectContext { @nonobjc internal func deleteAll(from: CSFrom, _ deleteClauses: [CSDeleteClause]) -> Int? { - let fetchRequest = NSFetchRequest() + let fetchRequest = CoreStoreFetchRequest() from.bridgeToSwift.applyToFetchRequest(fetchRequest, context: self) fetchRequest.fetchLimit = 0 @@ -109,4 +109,33 @@ internal extension NSManagedObjectContext { return self.deleteAll(fetchRequest) } + + @nonobjc + internal func queryValue(from: CSFrom, _ selectClause: CSSelect, _ queryClauses: [CSQueryClause]) -> AnyObject? { + + let fetchRequest = CoreStoreFetchRequest() + from.bridgeToSwift.applyToFetchRequest(fetchRequest, context: self) + + fetchRequest.fetchLimit = 0 + + let selectTerms = selectClause.selectTerms + selectTerms.applyToFetchRequest(fetchRequest, owner: selectClause) + queryClauses.forEach { $0.applyToFetchRequest(fetchRequest) } + + return self.queryValue(selectTerms, fetchRequest: fetchRequest) + } + + @nonobjc + internal func queryAttributes(from: CSFrom, _ selectClause: CSSelect, _ queryClauses: [CSQueryClause]) -> [[NSString: AnyObject]]? { + + let fetchRequest = CoreStoreFetchRequest() + from.bridgeToSwift.applyToFetchRequest(fetchRequest, context: self) + + fetchRequest.fetchLimit = 0 + + selectClause.selectTerms.applyToFetchRequest(fetchRequest, owner: selectClause) + queryClauses.forEach { $0.applyToFetchRequest(fetchRequest) } + + return self.queryAttributes(fetchRequest) + } }