Skip to main content
DatabaseRegionObservation is a low-level API for observing changes to specific database regions without fetching values.

Overview

While ValueObservation automatically fetches fresh values, DatabaseRegionObservation only notifies you that a region has changed:
let observation = DatabaseRegionObservation(tracking: Player.all())

observation.start(in: dbQueue) { error in
    print("Error: \(error)")
} onChange: {
    print("The player table was modified")
    // You decide when and how to fetch
}
Most applications should use ValueObservation instead. Use DatabaseRegionObservation only when you need fine-grained control over when values are fetched.

Creating Observations

tracking(_:)

Observes a single database region:
// Observe the entire player table
let observation = DatabaseRegionObservation(tracking: Player.all())

// Observe specific rows
let observation = DatabaseRegionObservation(
    tracking: Player.filter(Column("teamId") == 1)
)

// Observe specific columns
let observation = DatabaseRegionObservation(
    tracking: Player.select(Column("score"))
)

tracking(::)

Observes multiple regions:
let observation = DatabaseRegionObservation(
    tracking: Player.all(),
    Team.all()
)

Database Regions

A database region represents a set of database rows and columns:

Table Regions

// Entire table
DatabaseRegionObservation(tracking: Table("player"))

// All rows from a record type
DatabaseRegionObservation(tracking: Player.all())

Row Regions

// Specific rows by primary key
DatabaseRegionObservation(tracking: Player.filter(key: [1, 2, 3]))

// Rows matching a filter
DatabaseRegionObservation(tracking: Player.filter(Column("teamId") == 1))

Column Regions

// Specific columns
DatabaseRegionObservation(
    tracking: Player.select(Column("name"), Column("score"))
)

Full Database

DatabaseRegionObservation(tracking: DatabaseRegion.fullDatabase)

Starting Observations

start(in:onError:onChange:)

Starts observing database changes:
reader
DatabaseReader
required
The database queue or pool
onError
(Error) -> Void
required
Called when an error occurs
onChange
() -> Void
required
Called when the observed region changes
let observation = DatabaseRegionObservation(tracking: Player.all())

let cancellable = observation.start(
    in: dbQueue,
    onError: { error in
        print("Error: \(error)")
    },
    onChange: {
        print("Player table changed")
        // Fetch fresh data when needed
    }
)

Use Cases

Throttled Updates

Fetch values only when appropriate:
class PlayerManager {
    private var needsRefresh = false
    private var observation: DatabaseRegionObservation
    private var cancellable: AnyCancellable?
    
    init(dbQueue: DatabaseQueue) {
        observation = DatabaseRegionObservation(tracking: Player.all())
        
        cancellable = observation.start(
            in: dbQueue,
            onError: { _ in },
            onChange: { [weak self] in
                self?.needsRefresh = true
            }
        )
    }
    
    func refreshIfNeeded(_ db: Database) throws -> [Player] {
        guard needsRefresh else {
            return cachedPlayers
        }
        
        let players = try Player.fetchAll(db)
        cachedPlayers = players
        needsRefresh = false
        return players
    }
}

Coalescing Multiple Changes

let observation = DatabaseRegionObservation(tracking: Player.all())

var updateTimer: Timer?

let cancellable = observation.start(
    in: dbQueue,
    onError: { _ in },
    onChange: {
        // Coalesce rapid changes
        updateTimer?.invalidate()
        updateTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { _ in
            refreshUI()
        }
    }
)

Custom Scheduling

let observation = DatabaseRegionObservation(tracking: Player.all())

let queue = DispatchQueue(label: "updates")

let cancellable = observation.start(
    in: dbQueue,
    onError: { _ in },
    onChange: {
        queue.async {
            // Fetch and process on custom queue
            self.fetchAndProcess()
        }
    }
)

Combining with ValueObservation

Use both for different purposes:
// Automatic UI updates
let valueObservation = ValueObservation.tracking { db in
    try Player.fetchCount(db)
}

let valueCancellable = valueObservation.start(
    in: dbQueue,
    onError: { _ in },
    onChange: { count in
        updateCountLabel(count)
    }
)

// Manual refresh trigger
let regionObservation = DatabaseRegionObservation(tracking: Player.all())

let regionCancellable = regionObservation.start(
    in: dbQueue,
    onError: { _ in },
    onChange: {
        showRefreshIndicator()
    }
)

Performance Considerations

DatabaseRegionObservation has very low overhead - it only tracks changes without fetching data.
Use narrow regions when possible to reduce unnecessary notifications:
// Better - only notified when score changes
DatabaseRegionObservation(tracking: Player.select(Column("score")))

// Less efficient - notified for any column change
DatabaseRegionObservation(tracking: Player.all())

Limitations

DatabaseRegionObservation does not provide the changed values. You must fetch them yourself when notified.
Changes from external processes (other apps modifying the database) are not detected unless you use DatabasePool.invalidateReadOnlyConnections().

Cancellation

let cancellable = observation.start(
    in: dbQueue,
    onError: { _ in },
    onChange: { }
)

// Stop observing
cancellable.cancel()
Observations are automatically cancelled when the cancellable is deallocated.

Error Handling

let cancellable = observation.start(
    in: dbQueue,
    onError: { error in
        if let dbError = error as? DatabaseError {
            print("Database error: \(dbError.message ?? "")")
        }
    },
    onChange: {
        // Handle change
    }
)
When an error occurs, the observation stops and must be recreated.

See Also

Build docs developers (and LLMs) love