Simpler ways to write queries
Hi,
The current way to write queries seem to be pretty verbose with requiring a separate struct and the default value being inside the query struct which can get a bit annoying when needing to write lots of these all over your codebase.
Is it possible for a simpler design to be added similar to this quick mockup?
// Before
struct UserCountQuery: ValueObservationQueryable {
static var defaultValue: Int = 0
func fetch(_ db: Database) throws -> Int {
try User.fetchCount(db)
}
}
@Query(UserCountQuery()) var userCount: Int
// After
@Query({ db in
try User.fetchCount(db)
})
var userCount: Int = 0
Hello @Zomatree,
This is not currently possible, given the static declaration of defaultValue in the Queryable protocol.
But I could make a rapid prototype that enables what you aim at:
struct MyView: View {
@Query({ db in try User.fetchCount(db) }) // <-
var userCount = 0
var body: some View {
Text("\(userCount) users")
}
}
// MARK: - Support
extension Query where Request.Context == DatabaseContext {
init<Value>(
wrappedValue: Value,
_ fetch: @escaping @Sendable (Database) throws -> Value
)
where Request == AnyObservableRequest<Value>
{
let queryable = AnyObservableRequest(
defaultValue: wrappedValue,
performFetch: fetch)
self.init(queryable)
}
}
struct AnyObservableRequest<Value: Sendable>: ValueObservationQueryable {
var defaultValue: Value // <- not static
var performFetch: @Sendable (Database) throws -> Value
func fetch(_ db: Database) throws -> Value {
try performFetch(db)
}
static func == (lhs: Self, rhs: Self) -> Bool {
// Here we tell GRDBQuery that there is
// no parameter that could change
// and that observation never needs to
// be restarted.
//
// This is ok for a prototype, but not
// in general: we break the substitutability
// rule of the Equatable protocol.
true
}
}
In all honesty, I don't know if this convenience should ship in the library, because it does not scale well with growing application requirements.
Say you now have to count the specified subset of users, what would you write? The initial state of the code has no natural path for growth:
struct MyView: View {
@Query(???)
var userCount = 0
init(registeredUsers: Bool) { ??? }
}
With the extra type, I find it easier to evolve the app, using the existing developer knowledge:
struct UserCountQuery: ValueObservationQueryable {
static var defaultValue: Int = 0
+ var registered: Bool
func fetch(_ db: Database) throws -> Int {
- try User.fetchCount(db)
+ try User
+ .filter(Column("registered") == registered)
+ .fetchCount(db)
}
}
struct MyView: View {
- @Query(UserCountQuery()) var userCount: Int
+ @Query<UserCountQuery> var userCount: Int
+ init(registeredUsers: Bool) {
+ _userCount = Query(UserCountQuery(registered: registeredUsers))
+ }
}
I understand that this is debatable. But designing for app growth is one of my hobbies 😅 I very much dislike threshold effects and tipping points, and like that expectable small changes in app complexity can be implemented by extending the current code, instead of a complete rewrite. APIs that can't handle mundane changes without a complete rewrite are frustrating because they fail to build on the knowledge that has been acquired so far, and fail to show respect to the time that the developer has generously given to study.
Anyway, I was looking for a reason to make defaultValue an instance property instead of a static property, and you just gave me one. Even if GRDBQuery does not ship AnyObservableRequest, you would be able to add it to your app and get the convenience you're after.
Another reason why the extra type is desirable is that a @Query initializer that is "too convenient" would drive people to write multiple @Query per view, which is almost always undesired, because this drops all guarantees of database integrity. The "good" way to is to fetch all the desired value from one Queryable type.
Closing due to inactivity