Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OptionalQueryable & ListQueryable helper protocols #39

Closed
DanielSincere opened this issue Mar 16, 2023 · 4 comments
Closed

OptionalQueryable & ListQueryable helper protocols #39

DanielSincere opened this issue Mar 16, 2023 · 4 comments

Comments

@DanielSincere
Copy link

DanielSincere commented Mar 16, 2023

I've used GRDBQuery on several projects now, it's a good package.

I endup pasting the following two helpers into all of my projects. I noticed that GRDBQuery doesn't depend on GRDB, so I can't directly open a pull request to share these. Writing about them here in case some one might find them useful, or might find a way to include them in either GRDB or GRDBQuery.

import GRDB
import GRDBQuery
import Combine

protocol OptionalQueryable: Queryable {
  associatedtype V
  func fetch(_ db: Database) throws -> V?
}

extension OptionalQueryable where DatabaseContext == DatabaseWriter, ValuePublisher == AnyPublisher<V?, Error> {

  static var defaultValue: V? { nil }

  func publisher(in database: DatabaseWriter) -> AnyPublisher<V?, Error> {
    ValueObservation
      .tracking(regions: [.fullDatabase], fetch: fetch)
      .publisher(in: database, scheduling: .immediate)
      .eraseToAnyPublisher()
  }
}

protocol ListQueryable: Queryable {
  associatedtype V
  func fetch(_ db: Database) throws -> [V]
}

extension ListQueryable {

  static var defaultValue: [V] { [] }

  func publisher(in database: DatabaseWriter) -> AnyPublisher<[V], Error> {
    ValueObservation
      .tracking(fetch)
      .publisher(in: database, scheduling: .immediate)
      .eraseToAnyPublisher()
  }
}

Example

Where MyRecord conforms to Codable and FetchableRecord, and where MyRecord.CodingKeys conforms to ColumnExpression.

extension MyRecord {
  struct FetchByID: OptionalQueryable {

    let id: MyRecord.ID

    func fetch(_ db: Database) throws -> MyRecord? {
      try MyRecord
        .filter(MyRecord.CodingKeys.id == id)
        .fetchOne(db)
    }
  }
}

Testing

Also, with these helpers, it's easy to write tests around the query.

final class MyQueryTest: XCTestCase {

  func testMyQuery() throws {

    let result = try AppDatabase.fixture().read { db in
      try MyQuery().fetch(db)
    }

    XCTAssertEqual(result.count, 3)
  }
}

Where AppDatabase.fixture() is a DatabaseQueue with the migrations run and some fixtures added.

@DanielSincere
Copy link
Author

Perhaps the scheduling and regions parameters could be static vars on each protocol to make them configurable.

@groue
Copy link
Owner

groue commented Mar 16, 2023

Hello @FullQueueDeveloper,

Thank you :-)

These sub-protocols can live and work when embedded in your app, and I think this is their best home. They represent your own practices, which, while totally valid, are quite specific to your current setup.

For example, you have noticed that people might want to use a different configuration of the database publishers. Why not keep things simple? Your protocols fit your needs without this extra configuration complexity, and this looks pretty satisfying to me.

Also, not everybody puts a raw DatabaseWriter type in the environment. Some put a "repository", or a "database manager", or whatever object they want. It's better if the library does not go too far in fostering any particular practice in this regard. You know there exists a wild landscape of app architectures and "best ways to build an app", and people can be very opinionated about that. Big apps can indeed use a little discipline. In the same time, some people don't bother, or don't have time, or are setting up a small demo, and just want something that works. Well, everyone is welcome :-)

I hope users will stumble on this issue, get inspired by your protocols, and tune them for their own apps!

@groue
Copy link
Owner

groue commented Mar 16, 2023

Some extra comments:

Your testing setup is good, I like it 👍

.tracking(regions: [.fullDatabase], fetch: fetch)

Doesn't it work with a plain .tracking(fetch)? When you explicitly track the .fullDatabase region, all database changes trigger a refresh, even changes that do not impact the fetched value. Is it your actual intent?

@DanielSincere
Copy link
Author

Ah not sure which query it was that needed that, but you're right, it's not a sensible default.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants