Detect Changes to a Column

ValueObservation seems to detect changes that would impact fetchAll() but when I change a column attribute, the observer callback from ValueObservation does not fire. For example, I have a MbyCourse with a Bool archived column. when I update the column from false to true I no longer need to show the MbyCourse in the UI. My current ValueObservation code: ValueObservation.swift · GitHub

How do I detect changes to a column?

I saw TransactionObserver Protocol in the docs(GitHub - groue/GRDB.swift: A toolkit for SQLite databases, with a focus on application development), but I'm not sure I need that fine-grained control if DatabaseRegionObservation(GitHub - groue/GRDB.swift: A toolkit for SQLite databases, with a focus on application development) is more appropriate. Or maybe there is actually a way to use ValueObservation to get that degree of fine-grained database column change detection?

Here is my model:

import Foundation
import GRDB

public struct MbyCourse: Codable, CSVObjectConstruct {
    public var uuid: String
    public var orgUuid: String
    public var courseCodeUuid: String
    public var code: String
    public var title: String
    public var term: String
    public var archived: Bool
    //
    // a MbyCourse is many-to-one to MbyOrg
    //
    static let mbyOrgForeignKey = ForeignKey([Columns.orgUuid])
    
    //
    // a MbyCourse is one-to-many to MbyCourseSection
    //
    static let sections = hasMany(MbyCourseSection.self, using: MbyCourseSection.mbyCourseForeignKey)

    var sections: QueryInterfaceRequest<MbyCourseSection> {
        return request(for: MbyCourse.sections)
    }
}

// Define columns so that we can build GRDB requests
extension MbyCourse {
    enum Columns {
        static let uuid = Column("uuid")
        static let orgUuid = Column("orgUuid")
        static let courseCodeUuid = Column("courseCodeUuid")
        static let code = Column("code")
        static let title = Column("title")
        static let term = Column("term")
        static let archived = Column("archived")
    }
}

// Adopt RowConvertible so that we can fetch players from the database.
// Implementation is automatically derived from Codable.
extension MbyCourse: FetchableRecord, PersistableRecord { }

Here is the code-snippet which does the column change:

    public func archiveCourse(forUuid: String) -> Single<Void> {
        return Single.create { [unowned self] single in
            do {
                var archiveCourse: MbyCourse?
                
                try self.dbPool.read { db in
                        archiveCourse = try MbyCourse.filter(Column("uuid") == forUuid).fetchOne(db)
                }
                
                try self.dbPool.writeInTransaction { db in
                    archiveCourse?.archived = true
                    try archiveCourse?.update(db)
                    return .commit
                }
                single(.success(()))
            } catch {
                print("error: \(error)")
                single(.error(error))
            }
            return Disposables.create {}
        }
    }

Hello @mazz

when I change a column attribute, the observer callback from ValueObservation does not fire.

Then you may have found a bug. Bugs should be declared as a Github issue. Since ValueObservation is already well tested, and observes tables, rows, and columns just fine as far as I can tell, please come with some evidence of your claim, which is something reproducible. For example, a minimal playground, or project. Please remove useless details and dependencies.

// (In a single file)
// 1. Setup a db
// 2. Start an observation
// 3. Perform a change
// 4. Wait for the change callback (which never comes if there's a bug)

It looks correct :+1: I can suggest a simplification, and a clarification.

The ValueObservation.tracking(value:) method accepts a throwing closure. Let it catch db errors, because it is its job. All the sample codes in the documentation do so, did you notice? Error handling should be performed in the onError callback of the start method. This gives:

// Don't handle db errors in observations
let coursesObservation = ValueObservation.tracking { db in
    try MbyCourse
        .filter(Column("orgUuid") == self.orgUuid.value)
        .filter(Column("archived") == false)
        .fetchAll(db)
}

Now, the request depends on self.orgUuid.value. If the value of this property changes, the observation won't be "kicked" - it's not a database change, after all. But the code above looks like it does. This illusion can be lifted by extracting the observed request:

// Make it more clear which request is observed
let request = MbyCourse
    .filter(Column("orgUuid") == orgUuid.value)
    .filter(Column("archived") == false)
let coursesObservation = ValueObservation.tracking { db in
    try request.fetchAll(db)
}

I've determined yet again this was 'pilot error'. The view model I generated which holds the instance of the ValueObserver would get de-allocated by Swinject. I do not know why Swinject is doing this because I set the Swinject register() of the view model to .container which means it should persist. I did a workaround to fix this. Sorry about the false alarm.