You can backup (copy) a database into another database. This is useful for creating backups, copying in-memory databases to files, or implementing document-based applications.
Basic Backup
The backup method copies the entire contents of one database to another:
let source: DatabaseQueue = try DatabaseQueue(path: "/path/to/source.db")
let destination: DatabaseQueue = try DatabaseQueue(path: "/path/to/backup.db")
try source.backup(to: destination)
You can also backup between DatabasePool instances:
let source: DatabasePool = try DatabasePool(path: "/path/to/source.db")
let destination: DatabasePool = try DatabasePool(path: "/path/to/backup.db")
try source.backup(to: destination)
The backup method blocks the current thread until the destination database contains the same contents as the source database.
Backup Behavior
When the source is a DatabasePool, concurrent writes can happen during the backup:
- Those writes may or may not be reflected in the backup
- They won’t trigger any errors
- The backup represents a consistent state of the database
Database-Level Backup
For more control, use the Database.backup method:
let source: DatabaseQueue = ...
let destination: DatabaseQueue = ...
try source.write { sourceDb in
try destination.barrierWriteWithoutTransaction { destDb in
try sourceDb.backup(to: destDb)
}
}
This allows you to choose specific source and destination Database handles for the backup operation.
Common Use Cases
Backing Up to a File
Create a backup of your app’s database:
let appDB = try DatabaseQueue(path: "/path/to/app.db")
let backupDB = try DatabaseQueue(path: "/path/to/backup.db")
try appDB.backup(to: backupDB)
print("Backup completed")
Copying In-Memory to File
Persist an in-memory database to disk:
let memoryDB = try DatabaseQueue()
let fileDB = try DatabaseQueue(path: "/path/to/saved.db")
// Work with in-memory database
try memoryDB.write { db in
try db.execute(sql: "CREATE TABLE player (name TEXT)")
try db.execute(sql: "INSERT INTO player VALUES ('Arthur')")
}
// Save to file
try memoryDB.backup(to: fileDB)
Loading File into Memory
Load a database file into memory for fast access:
let fileDB = try DatabaseQueue(path: "/path/to/database.db")
let memoryDB = try DatabaseQueue()
// Load into memory
try fileDB.backup(to: memoryDB)
// Work with in-memory copy
try memoryDB.read { db in
let players = try Player.fetchAll(db)
}
Document-Based Applications
Implement NSDocument subclasses with database backing:
class DatabaseDocument: NSDocument {
private var database: DatabaseQueue?
override func read(from url: URL, ofType typeName: String) throws {
let fileDB = try DatabaseQueue(path: url.path)
let memoryDB = try DatabaseQueue()
try fileDB.backup(to: memoryDB)
self.database = memoryDB
}
override func write(to url: URL, ofType typeName: String) throws {
guard let memoryDB = database else { return }
let fileDB = try DatabaseQueue(path: url.path)
try memoryDB.backup(to: fileDB)
}
}
Progress Reporting
Track backup progress with the pagesPerStep and progress parameters.
Basic Progress Tracking
try source.backup(
to: destination,
pagesPerStep: 50
) { backupProgress in
print("Remaining pages: \(backupProgress.pageCount - backupProgress.pageIndex)")
print("Total pages: \(backupProgress.pageCount)")
print("Progress: \(backupProgress.pageIndex)/\(backupProgress.pageCount)")
print("Complete: \(backupProgress.isComplete)")
}
How It Works
When pagesPerStep is provided:
- The backup is performed in steps
- Each step copies at most
pagesPerStep database pages
- The
progress callback is called after each step
- The callback is also called after the final step
The backupProgress parameter provides:
pageIndex - Number of pages copied so far
pageCount - Total number of pages to copy
isComplete - Whether the backup is finished
Progress UI Example
func backupDatabase(from source: DatabaseQueue,
to destination: DatabaseQueue,
progressHandler: @escaping (Double) -> Void) throws {
try source.backup(
to: destination,
pagesPerStep: 100
) { progress in
let percentage = Double(progress.pageIndex) / Double(progress.pageCount)
DispatchQueue.main.async {
progressHandler(percentage)
}
}
}
// Usage
try backupDatabase(from: appDB, to: backupDB) { percentage in
progressBar.progress = Float(percentage)
progressLabel.text = "\(Int(percentage * 100))%"
}
Aborting a Backup
You can abort an incomplete backup by throwing an error from the progress callback:
struct BackupCancelled: Error { }
var shouldCancel = false
do {
try source.backup(
to: destination,
pagesPerStep: 100
) { progress in
if shouldCancel && !progress.isComplete {
throw BackupCancelled()
}
}
} catch is BackupCancelled {
print("Backup was cancelled")
}
If the progress callback throws when backupProgress.isComplete == true, the error is silently ignored because the backup has already completed.
Advanced Options
Custom Page Size
Choose an appropriate pagesPerStep based on your needs:
// Fast backup with less frequent progress updates
try source.backup(to: destination, pagesPerStep: 1000) { progress in
// Called every 1000 pages
}
// Slower backup with more frequent progress updates
try source.backup(to: destination, pagesPerStep: 10) { progress in
// Called every 10 pages
}
Larger pagesPerStep values mean faster backups but less frequent progress updates. Smaller values provide smoother progress updates but may slow down the backup.
Backup with Retries
func backupWithRetry(from source: DatabaseQueue,
to destination: DatabaseQueue,
maxAttempts: Int = 3) throws {
var lastError: Error?
for attempt in 1...maxAttempts {
do {
try source.backup(to: destination)
return
} catch {
lastError = error
print("Backup attempt \(attempt) failed: \(error)")
if attempt < maxAttempts {
Thread.sleep(forTimeInterval: 1.0)
}
}
}
throw lastError!
}
Best Practices
- Use appropriate backup timing - Don’t backup too frequently
- Handle errors - Backup operations can fail
- Consider disk space - Ensure enough space for the backup
- Test restore procedures - Verify backups can be restored
- Backup before migrations - Create backups before schema changes
- Use progress reporting for large databases - Keep users informed
- Clean up old backups - Don’t accumulate unlimited backups
Backup Encrypted Databases
When working with encrypted databases, both source and destination must use the correct passphrases:
// Backup encrypted database
var sourceConfig = Configuration()
sourceConfig.prepareDatabase { db in
try db.usePassphrase("source-secret")
}
let source = try DatabaseQueue(path: "/path/to/encrypted.db",
configuration: sourceConfig)
var destConfig = Configuration()
destConfig.prepareDatabase { db in
try db.usePassphrase("dest-secret")
}
let destination = try DatabaseQueue(path: "/path/to/backup.db",
configuration: destConfig)
try source.backup(to: destination)
Important Notes
Passing non-default values of pagesPerStep or progress is an advanced API intended for expert users. GRDB’s backup API is a low-level wrapper around SQLite’s online backup API. This documentation is not a substitute for the official SQLite documentation.
See Also