Skip to content

Commit

Permalink
Added ability to backup whole database
Browse files Browse the repository at this point in the history
  • Loading branch information
andreasley committed Aug 25, 2024
1 parent 8e13708 commit e7bc9b2
Show file tree
Hide file tree
Showing 2 changed files with 75 additions and 0 deletions.
51 changes: 51 additions & 0 deletions Sources/Blackbird/BlackbirdDatabase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ extension Blackbird {
case cannotOpenDatabaseAtPath(path: String, description: String)
case unsupportedConfigurationAtPath(path: String)
case queryError(query: String, description: String)
case backupError(description: String)
case queryArgumentNameError(query: String, name: String)
case queryArgumentValueError(query: String, description: String)
case queryExecutionError(query: String, description: String)
Expand Down Expand Up @@ -481,6 +482,14 @@ extension Blackbird {

public func setArtificialQueryDelay(_ delay: TimeInterval?) async { await core.setArtificialQueryDelay(delay) }

/// Creates a backup of the whole database.
///
/// - Parameters:
/// - targetPath: The path to the backup file to be created.
/// - pagesPerStep: The number of [pages](https://www.sqlite.org/fileformat.html#pages) to copy in a single step (optional; defaults to 100).
///
/// An error will be thrown if a file already exists at `targetPath`, the backup database cannot be created or the backup process fails.
public func backup(to targetPath: String, pagesPerStep: Int32 = 100) async throws { try await core.backup(to: targetPath, pagesPerStep: pagesPerStep) }

// MARK: - Core

Expand Down Expand Up @@ -856,6 +865,48 @@ extension Blackbird {
}
return rows
}

public func backup(to targetPath: String, pagesPerStep: Int32, printProgress: Bool = false) async throws {
guard !FileManager.default.fileExists(atPath: targetPath) else {
throw Blackbird.Database.Error.backupError(description: "File already exists at `\(targetPath)`")
}

var targetDbHandle: OpaquePointer? = nil
let flags: Int32 = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE
let openResult = sqlite3_open_v2(targetPath, &targetDbHandle, flags, nil)

guard let targetDbHandle else {
throw Error.cannotOpenDatabaseAtPath(path: targetPath, description: "SQLite cannot allocate memory")
}

guard openResult == SQLITE_OK else {
let code = sqlite3_errcode(targetDbHandle)
let msg = String(cString: sqlite3_errmsg(targetDbHandle), encoding: .utf8) ?? "(unknown)"
sqlite3_close(targetDbHandle)
throw Error.cannotOpenDatabaseAtPath(path: targetPath, description: "SQLite error code \(code): \(msg)")
}

guard let backup = sqlite3_backup_init(targetDbHandle, "main", dbHandle, "main") else {
throw Blackbird.Database.Error.backupError(description: errorDesc(targetDbHandle))
}

var stepResult = SQLITE_OK
while stepResult == SQLITE_OK || stepResult == SQLITE_BUSY || stepResult == SQLITE_LOCKED {
stepResult = sqlite3_backup_step(backup, pagesPerStep)

if printProgress {
let remainingPages = sqlite3_backup_remaining(backup)
let totalPages = sqlite3_backup_pagecount(backup)
let backedUpPages = totalPages - remainingPages
print("Backed up \(backedUpPages) pages of \(totalPages)\n")
}

await Task.yield()
}

sqlite3_backup_finish(backup)
sqlite3_close(targetDbHandle)
}
}
}

Expand Down
24 changes: 24 additions & 0 deletions Tests/BlackbirdTests/BlackbirdTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1264,6 +1264,30 @@ final class BlackbirdTestTests: XCTestCase, @unchecked Sendable {
// }
}

func testBackup() async throws {
let db = try Blackbird.Database(path: sqliteFilename)
for i in 0..<1000 {
try await TestModel(id: Int64(i), title: TestData.randomTitle, url: TestData.randomURL).write(to: db)
}
let backupFilePath = sqliteFilename + ".backup"
print("Creating backup at \(backupFilePath)")

defer {
for path in Blackbird.Database.allFilePaths(for: backupFilePath) {
try? FileManager.default.removeItem(atPath: path)
}
}

try await db.core.backup(to: backupFilePath, pagesPerStep: 100, printProgress: true)

let backupDb = try Blackbird.Database(path: backupFilePath)

let modelsInDb = try await TestModel.read(from: db)
let modelsInBackupDb = try await TestModel.read(from: backupDb)

XCTAssert(modelsInDb == modelsInBackupDb)
}


/* Tests duplicate-index detection. Throws fatal error on success.
func testDuplicateIndex() async throws {
Expand Down

0 comments on commit e7bc9b2

Please sign in to comment.