diff --git a/Package.resolved b/Package.resolved index 55acb07..346093d 100644 --- a/Package.resolved +++ b/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/prosumma/PredicateQI", "state" : { - "revision" : "b4470fa9244631aa180b8b9cc7891c1fd7f979c6", - "version" : "1.0.2" + "revision" : "4730ae50948cd133cb386907cbce22bb7f2e1380", + "version" : "1.0.9" } } ], diff --git a/Package.swift b/Package.swift index 0d13b31..780235a 100644 --- a/Package.swift +++ b/Package.swift @@ -5,7 +5,7 @@ import PackageDescription let package = Package( name: "CoreDataQueryInterface", - platforms: [.macOS(.v12), .iOS(.v15), .tvOS(.v15), .watchOS(.v8)], + platforms: [.macOS(.v13), .iOS(.v15), .tvOS(.v15), .watchOS(.v8)], products: [ .library( name: "CoreDataQueryInterface", diff --git a/Sources/CoreDataQueryInterface/Attributed.swift b/Sources/CoreDataQueryInterface/Attributed.swift new file mode 100644 index 0000000..747d2c3 --- /dev/null +++ b/Sources/CoreDataQueryInterface/Attributed.swift @@ -0,0 +1,71 @@ +// +// Attributed.swift +// CoreDataQueryInterface +// +// Created by Greg Higley on 2023-05-13. +// + +import CoreData +import PredicateQI + +public protocol Attributed { + static var attributeType: NSAttributeDescription.AttributeType { get } +} + +extension TypedExpression: Attributed where T: Attributed { + public static var attributeType: NSAttributeDescription.AttributeType { + T.attributeType + } +} + +extension Bool: Attributed { + public static let attributeType: NSAttributeDescription.AttributeType = .boolean +} + +extension Data: Attributed { + public static let attributeType: NSAttributeDescription.AttributeType = .binaryData +} + +extension Date: Attributed { + public static let attributeType: NSAttributeDescription.AttributeType = .date +} + +extension String: Attributed { + public static let attributeType: NSAttributeDescription.AttributeType = .string +} + +extension Int16: Attributed { + public static let attributeType: NSAttributeDescription.AttributeType = .integer16 +} + +extension Int32: Attributed { + public static let attributeType: NSAttributeDescription.AttributeType = .integer32 +} + +extension Int64: Attributed { + public static let attributeType: NSAttributeDescription.AttributeType = .integer64 +} + +extension Decimal: Attributed { + public static let attributeType: NSAttributeDescription.AttributeType = .decimal +} + +extension Double: Attributed { + public static let attributeType: NSAttributeDescription.AttributeType = .double +} + +extension Float: Attributed { + public static let attributeType: NSAttributeDescription.AttributeType = .float +} + +extension NSManagedObjectID: Attributed { + public static let attributeType: NSAttributeDescription.AttributeType = .objectID +} + +extension UUID: Attributed { + public static let attributeType: NSAttributeDescription.AttributeType = .uuid +} + +extension URL: Attributed { + public static let attributeType: NSAttributeDescription.AttributeType = .uri +} diff --git a/Sources/CoreDataQueryInterface/NSExpressionDescription.swift b/Sources/CoreDataQueryInterface/NSExpressionDescription.swift index de88f2e..f6ca650 100644 --- a/Sources/CoreDataQueryInterface/NSExpressionDescription.swift +++ b/Sources/CoreDataQueryInterface/NSExpressionDescription.swift @@ -11,12 +11,21 @@ import PredicateQI public extension NSExpressionDescription { convenience init( objectKeyPath keyPath: KeyPath, V>, - name: String, - type: NSAttributeDescription.AttributeType + name: String? = nil, + type: NSAttributeDescription.AttributeType? = nil ) { self.init() - self.expression = Object()[keyPath: keyPath].pqiExpression - self.name = name - self.resultType = type + let expression = Object()[keyPath: keyPath].pqiExpression + self.expression = expression + if let name = name { + self.name = name + } else if expression.expressionType == .keyPath, let name = expression.keyPath.split(separator: ".").last { + self.name = String(name) + } + if let type = type { + self.resultType = type + } else if let a = V.self as? Attributed.Type { + self.resultType = a.attributeType + } } } diff --git a/Sources/CoreDataQueryInterface/NSManagedObjectID.swift b/Sources/CoreDataQueryInterface/NSManagedObjectID.swift index fae065d..78e8b23 100644 --- a/Sources/CoreDataQueryInterface/NSManagedObjectID.swift +++ b/Sources/CoreDataQueryInterface/NSManagedObjectID.swift @@ -8,4 +8,4 @@ import CoreData import PredicateQI -extension NSManagedObjectID: TypeComparable {} +extension NSManagedObjectID: ConstantExpression, TypeComparable {} diff --git a/Sources/CoreDataQueryInterface/QueryBuilder+Fetch.swift b/Sources/CoreDataQueryInterface/QueryBuilder+Fetch.swift index 336b811..6abf9ee 100644 --- a/Sources/CoreDataQueryInterface/QueryBuilder+Fetch.swift +++ b/Sources/CoreDataQueryInterface/QueryBuilder+Fetch.swift @@ -91,6 +91,10 @@ public extension QueryBuilder { try dictionaries().fetch(managedObjectContext) as! [[String: Any]] } + func fetchFirst(_ managedObjectContext: NSManagedObjectContext? = nil) throws -> R? { + try limit(1).fetch(managedObjectContext).first + } + func count(_ managedObjectContext: NSManagedObjectContext? = nil) throws -> Int { guard let moc = self.managedObjectContext ?? managedObjectContext else { preconditionFailure("No NSManagedObjectContext instance on which to execute the request.") diff --git a/Sources/CoreDataQueryInterface/QueryBuilder+Group.swift b/Sources/CoreDataQueryInterface/QueryBuilder+Group.swift index 1c6bfb3..470abcf 100644 --- a/Sources/CoreDataQueryInterface/QueryBuilder+Group.swift +++ b/Sources/CoreDataQueryInterface/QueryBuilder+Group.swift @@ -26,7 +26,7 @@ public extension QueryBuilder { group(by: properties) } - func group(by keyPath: KeyPath, name: String, type: NSAttributeDescription.AttributeType) -> QueryBuilder { + func group(by keyPath: KeyPath, name: String? = nil, type: NSAttributeDescription.AttributeType? = nil) -> QueryBuilder { return group(by: NSExpressionDescription(objectKeyPath: keyPath, name: name, type: type)) } diff --git a/Sources/CoreDataQueryInterface/QueryBuilder+Group.swift.gyb b/Sources/CoreDataQueryInterface/QueryBuilder+Group.swift.gyb index 29fd719..0f8579a 100644 --- a/Sources/CoreDataQueryInterface/QueryBuilder+Group.swift.gyb +++ b/Sources/CoreDataQueryInterface/QueryBuilder+Group.swift.gyb @@ -29,7 +29,7 @@ public extension QueryBuilder { group(by: properties) } - func group(by keyPath: KeyPath, name: String, type: NSAttributeDescription.AttributeType) -> QueryBuilder { + func group(by keyPath: KeyPath, name: String? = nil, type: NSAttributeDescription.AttributeType? = nil) -> QueryBuilder { return group(by: NSExpressionDescription(objectKeyPath: keyPath, name: name, type: type)) } diff --git a/Sources/CoreDataQueryInterface/QueryBuilder+Select.swift b/Sources/CoreDataQueryInterface/QueryBuilder+Select.swift index 72a2811..ed52519 100644 --- a/Sources/CoreDataQueryInterface/QueryBuilder+Select.swift +++ b/Sources/CoreDataQueryInterface/QueryBuilder+Select.swift @@ -32,16 +32,10 @@ public extension QueryBuilder { select(properties) } - func select(_ keyPath: KeyPath, name: String, type: NSAttributeDescription.AttributeType) -> QueryBuilder { + func select(_ keyPath: KeyPath, name: String? = nil, type: NSAttributeDescription.AttributeType? = nil) -> QueryBuilder { select(NSExpressionDescription(objectKeyPath: keyPath, name: name, type: type)) } - func select(_ keyPath: KeyPath) -> QueryBuilder { - let object = O() - let expression = object[keyPath: keyPath] - return select("\(expression.pqiExpression)") - } - func select( _ keyPath1: KeyPath, _ keyPath2: KeyPath diff --git a/Sources/CoreDataQueryInterface/QueryBuilder+Select.swift.gyb b/Sources/CoreDataQueryInterface/QueryBuilder+Select.swift.gyb index f026c22..b668ea6 100644 --- a/Sources/CoreDataQueryInterface/QueryBuilder+Select.swift.gyb +++ b/Sources/CoreDataQueryInterface/QueryBuilder+Select.swift.gyb @@ -36,15 +36,9 @@ public extension QueryBuilder { select(properties) } - func select(_ keyPath: KeyPath, name: String, type: NSAttributeDescription.AttributeType) -> QueryBuilder { + func select(_ keyPath: KeyPath, name: String? = nil, type: NSAttributeDescription.AttributeType? = nil) -> QueryBuilder { select(NSExpressionDescription(objectKeyPath: keyPath, name: name, type: type)) } - - func select(_ keyPath: KeyPath) -> QueryBuilder { - let object = O() - let expression = object[keyPath: keyPath] - return select("\(expression.pqiExpression)") - } % for i in range(2, 8): func select<${args(range(1, i + 1), lambda i: f'V{i}: E')}>( diff --git a/Sources/CoreDataQueryInterface/QueryBuilder.swift b/Sources/CoreDataQueryInterface/QueryBuilder.swift index 251216c..79cc7dc 100644 --- a/Sources/CoreDataQueryInterface/QueryBuilder.swift +++ b/Sources/CoreDataQueryInterface/QueryBuilder.swift @@ -25,9 +25,12 @@ public struct QueryBuilder { init(copying query: QueryBuilder) { managedObjectContext = query.managedObjectContext + fetchLimit = query.fetchLimit + fetchOffset = query.fetchOffset predicates = query.predicates sortDescriptors = query.sortDescriptors propertiesToFetch = query.propertiesToFetch + propertiesToGroupBy = query.propertiesToGroupBy returnsDistinctResults = query.returnsDistinctResults } diff --git a/Tests/CoreDataQueryInterfaceTests/QueryBuilderFilterTests.swift b/Tests/CoreDataQueryInterfaceTests/QueryBuilderFilterTests.swift index 91ab97c..d275d7e 100644 --- a/Tests/CoreDataQueryInterfaceTests/QueryBuilderFilterTests.swift +++ b/Tests/CoreDataQueryInterfaceTests/QueryBuilderFilterTests.swift @@ -1,6 +1,6 @@ // // QueryBuilderFilterTests.swift -// +// CoreDataQueryInterface // // Created by Greg Higley on 2022-10-22. // @@ -17,6 +17,16 @@ final class QueryBuilderFilterTests: XCTestCase { XCTAssertEqual(count, 2) } + func testFilterBySubquery() throws { + let moc = Store.container.viewContext + let filter: (Object) -> PredicateBuilder = { + // SUBQUERY(developers, $v, ANY $v.lastName BEGINSWITH[c] "d").@count > 0 + $0.developers.where { any(ci($0.lastName <~% "d")) } + } + let count = try moc.query(Language.self).filter(filter).count() + XCTAssertEqual(count, 2) + } + func testFilterWithArgs() throws { let moc = Store.container.viewContext let count = try moc.query(Language.self).filter("%K BEGINSWITH %@", "name", "R").count() diff --git a/Tests/CoreDataQueryInterfaceTests/QueryBuilderGroupTests.swift b/Tests/CoreDataQueryInterfaceTests/QueryBuilderGroupTests.swift index 50a8335..55fa0ed 100644 --- a/Tests/CoreDataQueryInterfaceTests/QueryBuilderGroupTests.swift +++ b/Tests/CoreDataQueryInterfaceTests/QueryBuilderGroupTests.swift @@ -14,11 +14,11 @@ final class QueryBuilderGroupTests: XCTestCase { let moc = Store.container.viewContext let query = Query(Developer.self) .group(by: \.lastName, \.firstName) - .select(\.firstName, \.lastName) - .select(\.languages.name.pqiCount, name: "count", type: .integer32) + .select(\.firstName, \.lastName, \.languages.name.pqiCount.pqiFloat) .filter { ci($0.lastName == "higley")} let result = try query.fetchDictionaries(moc) - let count = result[0]["count"] as! Int + print(result[0].keys) + let count = result[0]["@count"] as! Int64 XCTAssertEqual(count, 3) } } diff --git a/Tests/CoreDataQueryInterfaceTests/QueryBuilderOrderTests.swift b/Tests/CoreDataQueryInterfaceTests/QueryBuilderOrderTests.swift index f5e0c29..1c37f4f 100644 --- a/Tests/CoreDataQueryInterfaceTests/QueryBuilderOrderTests.swift +++ b/Tests/CoreDataQueryInterfaceTests/QueryBuilderOrderTests.swift @@ -16,4 +16,11 @@ final class QueryBuilderOrderTests: XCTestCase { XCTAssertFalse(languages.isEmpty) XCTAssertEqual(languages.first?.name, "Haskell") } + + func testOrderDescendingByName() throws { + let moc = Store.container.viewContext + let languages = try moc.query(Language.self).order(.descending, by: \.name).fetch() + XCTAssertFalse(languages.isEmpty) + XCTAssertEqual(languages.first?.name, "Visual Basic") + } } diff --git a/Tests/CoreDataQueryInterfaceTests/Store.swift b/Tests/CoreDataQueryInterfaceTests/Store.swift index 562bf43..cd33393 100644 --- a/Tests/CoreDataQueryInterfaceTests/Store.swift +++ b/Tests/CoreDataQueryInterfaceTests/Store.swift @@ -11,6 +11,8 @@ import PredicateQI enum Store { private static func initPersistentContainer() -> NSPersistentContainer { + PredicateQIConfiguration.logPredicatesToConsole = true + let mom = NSManagedObjectModel.mergedModel(from: [Bundle.module])! let container = NSPersistentContainer(name: "developers", managedObjectModel: mom) let store = NSPersistentStoreDescription(url: URL(fileURLWithPath: "/dev/null")) @@ -47,7 +49,7 @@ enum Store { let languageNames = info["ls"] as! [String] var languages: Set = [] for name in languageNames { - let language = try! moc.query(Language.self).filter { $0.name == name }.fetch().first! + let language = try! moc.query(Language.self).filter { $0.name == name }.fetchFirst()! languages.insert(language) } let developer = Developer(context: moc)