From 3f529f80ed872143de31a02c669005aeb143c9b2 Mon Sep 17 00:00:00 2001 From: Gregory Higley Date: Wed, 26 Oct 2022 17:17:48 -0400 Subject: [PATCH 1/5] Bad copy constructor - but somehow everything worked --- Sources/CoreDataQueryInterface/QueryBuilder.swift | 3 +++ 1 file changed, 3 insertions(+) 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 } From d37948276b2c1cce9b1c685125ef673dc9bf68e9 Mon Sep 17 00:00:00 2001 From: Gregory Higley Date: Thu, 27 Oct 2022 13:21:54 -0400 Subject: [PATCH 2/5] fetchFirst --- Sources/CoreDataQueryInterface/QueryBuilder+Fetch.swift | 4 ++++ Tests/CoreDataQueryInterfaceTests/Store.swift | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) 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/Tests/CoreDataQueryInterfaceTests/Store.swift b/Tests/CoreDataQueryInterfaceTests/Store.swift index 562bf43..857c6b5 100644 --- a/Tests/CoreDataQueryInterfaceTests/Store.swift +++ b/Tests/CoreDataQueryInterfaceTests/Store.swift @@ -47,7 +47,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) From 22211b7540ebd89320977a5a936948b83ef7309f Mon Sep 17 00:00:00 2001 From: Gregory Higley Date: Sat, 13 May 2023 01:24:09 -0400 Subject: [PATCH 3/5] Add debugging output for predicates --- Package.resolved | 4 ++-- .../QueryBuilderFilterTests.swift | 12 +++++++++++- Tests/CoreDataQueryInterfaceTests/Store.swift | 2 ++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/Package.resolved b/Package.resolved index 55acb07..d37e024 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" : "bf8351a4f1b9ba05de346bb47bfd8af590c92bcb", + "version" : "1.0.5" } } ], 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/Store.swift b/Tests/CoreDataQueryInterfaceTests/Store.swift index 857c6b5..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")) From 9aa0e4106dc92e0574cdd36533734fcb87182e08 Mon Sep 17 00:00:00 2001 From: Gregory Higley Date: Sat, 13 May 2023 11:27:44 -0400 Subject: [PATCH 4/5] Better implicit typing and naming with select/NSExpressionDescription --- Package.resolved | 4 +-- Package.swift | 2 +- .../CoreDataQueryInterface/Attributed.swift | 35 +++++++++++++++++++ .../NSExpressionDescription.swift | 21 ++++++++--- .../QueryBuilder+Select.swift | 8 +---- .../QueryBuilder+Select.swift.gyb | 8 +---- .../QueryBuilderOrderTests.swift | 7 ++++ 7 files changed, 63 insertions(+), 22 deletions(-) create mode 100644 Sources/CoreDataQueryInterface/Attributed.swift diff --git a/Package.resolved b/Package.resolved index d37e024..55acb07 100644 --- a/Package.resolved +++ b/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/prosumma/PredicateQI", "state" : { - "revision" : "bf8351a4f1b9ba05de346bb47bfd8af590c92bcb", - "version" : "1.0.5" + "revision" : "b4470fa9244631aa180b8b9cc7891c1fd7f979c6", + "version" : "1.0.2" } } ], 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..8aaff32 --- /dev/null +++ b/Sources/CoreDataQueryInterface/Attributed.swift @@ -0,0 +1,35 @@ +// +// 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 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 +} diff --git a/Sources/CoreDataQueryInterface/NSExpressionDescription.swift b/Sources/CoreDataQueryInterface/NSExpressionDescription.swift index de88f2e..07ae4cc 100644 --- a/Sources/CoreDataQueryInterface/NSExpressionDescription.swift +++ b/Sources/CoreDataQueryInterface/NSExpressionDescription.swift @@ -11,12 +11,23 @@ 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 { + if 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/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/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") + } } From 0eea4c64803bdb8d03debb3ea468efcab4d31a47 Mon Sep 17 00:00:00 2001 From: Gregory Higley Date: Sun, 14 May 2023 09:25:05 -0400 Subject: [PATCH 5/5] Refinements --- Package.resolved | 4 +-- .../CoreDataQueryInterface/Attributed.swift | 36 +++++++++++++++++++ .../NSExpressionDescription.swift | 6 ++-- .../NSManagedObjectID.swift | 2 +- .../QueryBuilder+Group.swift | 2 +- .../QueryBuilder+Group.swift.gyb | 2 +- .../QueryBuilderGroupTests.swift | 6 ++-- 7 files changed, 46 insertions(+), 12 deletions(-) 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/Sources/CoreDataQueryInterface/Attributed.swift b/Sources/CoreDataQueryInterface/Attributed.swift index 8aaff32..747d2c3 100644 --- a/Sources/CoreDataQueryInterface/Attributed.swift +++ b/Sources/CoreDataQueryInterface/Attributed.swift @@ -18,6 +18,18 @@ extension TypedExpression: Attributed where T: Attributed { } } +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 } @@ -33,3 +45,27 @@ extension Int32: Attributed { 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 07ae4cc..f6ca650 100644 --- a/Sources/CoreDataQueryInterface/NSExpressionDescription.swift +++ b/Sources/CoreDataQueryInterface/NSExpressionDescription.swift @@ -19,10 +19,8 @@ public extension NSExpressionDescription { self.expression = expression if let name = name { self.name = name - } else if expression.expressionType == .keyPath { - if let name = expression.keyPath.split(separator: "\\.").last { - self.name = String(name) - } + } else if expression.expressionType == .keyPath, let name = expression.keyPath.split(separator: ".").last { + self.name = String(name) } if let type = type { self.resultType = type 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+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/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) } }