Skip to content

Commit 30dcea9

Browse files
authored
Merge pull request #1075 from mallman/dev/issue-1074-incremental_backup_api
2 parents 042e220 + 2af0a74 commit 30dcea9

File tree

19 files changed

+423
-121
lines changed

19 files changed

+423
-121
lines changed

Documentation/AssociationsBasics.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -891,7 +891,7 @@ struct Author: TableRecord {
891891
static let databaseTableName = "writer"
892892
}
893893
struct Book: TableRecord {
894-
// Replace the defaut "writer" association key with "author"
894+
// Replace the default "writer" association key with "author"
895895
static let author = belongsTo(Author.self).forKey("author")
896896
}
897897

@@ -1034,7 +1034,7 @@ struct Book: TableRecord {
10341034
static let databaseTableName = "publication"
10351035
}
10361036
struct Author: TableRecord {
1037-
// Replace the defaut "publications" association key with "books"
1037+
// Replace the default "publications" association key with "books"
10381038
static let books = hasMany(Book.self).forKey("books")
10391039
}
10401040

GRDB.xcodeproj/project.pbxproj

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10+
4E13D2F32769B87F0037588C /* DatabaseBackupProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E13D2F22769B87F0037588C /* DatabaseBackupProgress.swift */; };
11+
4E13D2F42769B87F0037588C /* DatabaseBackupProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E13D2F22769B87F0037588C /* DatabaseBackupProgress.swift */; };
12+
4E13D2F52769B87F0037588C /* DatabaseBackupProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E13D2F22769B87F0037588C /* DatabaseBackupProgress.swift */; };
13+
4E13D2F62769B87F0037588C /* DatabaseBackupProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E13D2F22769B87F0037588C /* DatabaseBackupProgress.swift */; };
14+
4ED4BB592731DD25008B127D /* BackupTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ED4BB582731DD25008B127D /* BackupTestCase.swift */; };
15+
4ED4BB5A2731DD25008B127D /* BackupTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ED4BB582731DD25008B127D /* BackupTestCase.swift */; };
16+
4ED4BB5B2731DD25008B127D /* BackupTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ED4BB582731DD25008B127D /* BackupTestCase.swift */; };
1017
56012B552573EED000B4925B /* CommonTableExpressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56012B542573EED000B4925B /* CommonTableExpressionTests.swift */; };
1118
56012B562573EED100B4925B /* CommonTableExpressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56012B542573EED000B4925B /* CommonTableExpressionTests.swift */; };
1219
56012B572573EED100B4925B /* CommonTableExpressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56012B542573EED000B4925B /* CommonTableExpressionTests.swift */; };
@@ -1266,6 +1273,8 @@
12661273
/* End PBXContainerItemProxy section */
12671274

12681275
/* Begin PBXFileReference section */
1276+
4E13D2F22769B87F0037588C /* DatabaseBackupProgress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseBackupProgress.swift; sourceTree = "<group>"; };
1277+
4ED4BB582731DD25008B127D /* BackupTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupTestCase.swift; sourceTree = "<group>"; };
12691278
56012B542573EED000B4925B /* CommonTableExpressionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonTableExpressionTests.swift; sourceTree = "<group>"; };
12701279
56012B742574048B00B4925B /* CommonTableExpression.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonTableExpression.swift; sourceTree = "<group>"; };
12711280
560233C32724234F00529DF3 /* SharedValueObservation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedValueObservation.swift; sourceTree = "<group>"; };
@@ -2218,6 +2227,7 @@
22182227
56A238111B9C74A90082EB20 /* Core */ = {
22192228
isa = PBXGroup;
22202229
children = (
2230+
4ED4BB582731DD25008B127D /* BackupTestCase.swift */,
22212231
5665F85C203EE6030084C6C0 /* ColumnInfoTests.swift */,
22222232
56DAA2C41DE99D8D006E10C8 /* Cursor */,
22232233
56E4F7ED2392E2D000A611F6 /* DatabaseAbortedTransactionTests.swift */,
@@ -2324,6 +2334,7 @@
23242334
56A238711B9C75030082EB20 /* Database.swift */,
23252335
566B91221FA4CF810012D5B0 /* Database+Schema.swift */,
23262336
566B91081FA4C3960012D5B0 /* Database+Statements.swift */,
2337+
4E13D2F22769B87F0037588C /* DatabaseBackupProgress.swift */,
23272338
566B91121FA4C3F50012D5B0 /* DatabaseCollation.swift */,
23282339
56A238731B9C75030082EB20 /* DatabaseError.swift */,
23292340
564F9C2C1F075DD200877A00 /* DatabaseFunction.swift */,
@@ -2350,8 +2361,8 @@
23502361
56A238781B9C75030082EB20 /* Statement.swift */,
23512362
566B912A1FA4D0CC0012D5B0 /* StatementAuthorizer.swift */,
23522363
560D923F1C672C3E00F4F92B /* StatementColumnConvertible.swift */,
2353-
566B91321FA4D3810012D5B0 /* TransactionObserver.swift */,
23542364
5605F1471C672E4000235C62 /* Support */,
2365+
566B91321FA4D3810012D5B0 /* TransactionObserver.swift */,
23552366
);
23562367
path = Core;
23572368
sourceTree = "<group>";
@@ -2861,7 +2872,7 @@
28612872
);
28622873
runOnlyForDeploymentPostprocessing = 0;
28632874
shellPath = /bin/sh;
2864-
shellScript = "${PROJECT_DIR}/Scripts/swiftlint.sh\n";
2875+
shellScript = "\"${PROJECT_DIR}/Scripts/swiftlint.sh\"\n";
28652876
};
28662877
56EB54FA22C9115E00850069 /* Swiftlint */ = {
28672878
isa = PBXShellScriptBuildPhase;
@@ -2879,7 +2890,7 @@
28792890
);
28802891
runOnlyForDeploymentPostprocessing = 0;
28812892
shellPath = /bin/sh;
2882-
shellScript = "${PROJECT_DIR}/Scripts/swiftlint.sh\n";
2893+
shellScript = "\"${PROJECT_DIR}/Scripts/swiftlint.sh\"\n";
28832894
};
28842895
AAA4DCFA230F1E0600C74B15 /* Swiftlint */ = {
28852896
isa = PBXShellScriptBuildPhase;
@@ -2897,7 +2908,7 @@
28972908
);
28982909
runOnlyForDeploymentPostprocessing = 0;
28992910
shellPath = /bin/sh;
2900-
shellScript = "${PROJECT_DIR}/Scripts/swiftlint.sh\n";
2911+
shellScript = "\"${PROJECT_DIR}/Scripts/swiftlint.sh\"\n";
29012912
};
29022913
/* End PBXShellScriptBuildPhase section */
29032914

@@ -2913,6 +2924,7 @@
29132924
565490CE1D5AE252005622CB /* NSNull.swift in Sources */,
29142925
5656A8202295B12F001FF3FF /* SQLAssociation.swift in Sources */,
29152926
565490E41D5AE252005622CB /* TableRecord.swift in Sources */,
2927+
4E13D2F52769B87F0037588C /* DatabaseBackupProgress.swift in Sources */,
29162928
564CE5AE21B8FAB400652B19 /* DatabaseRegionObservation.swift in Sources */,
29172929
565490CF1D5AE252005622CB /* NSNumber.swift in Sources */,
29182930
56CEB51F1EAA328900BFAF62 /* FTS5+QueryInterface.swift in Sources */,
@@ -3116,6 +3128,7 @@
31163128
560D924C1C672C4B00F4F92B /* TableRecord.swift in Sources */,
31173129
56A2FA3B24424F4700E97D23 /* Export.swift in Sources */,
31183130
569BBA4F229170F900478429 /* Inflections+English.swift in Sources */,
3131+
4E13D2F42769B87F0037588C /* DatabaseBackupProgress.swift in Sources */,
31193132
5656A81F2295B12F001FF3FF /* SQLAssociation.swift in Sources */,
31203133
5617294F223533F40006E219 /* EncodableRecord.swift in Sources */,
31213134
563B8FAD24A1CE44007A48C9 /* DatabasePublishers.swift in Sources */,
@@ -3347,6 +3360,7 @@
33473360
563B8FA2249E8ACB007A48C9 /* ValueObservationPrintTests.swift in Sources */,
33483361
56A238581B9C74A90082EB20 /* RecordPrimaryKeyRowIDTests.swift in Sources */,
33493362
56A5EF131EF7F20B00F03071 /* ForeignKeyInfoTests.swift in Sources */,
3363+
4ED4BB5A2731DD25008B127D /* BackupTestCase.swift in Sources */,
33503364
5653EAE920944B4F00F46237 /* AssociationChainSQLTests.swift in Sources */,
33513365
5698ACD21DA8C2620056AF8C /* RecordPrimaryKeyHiddenRowIDTests.swift in Sources */,
33523366
561CFA7923735016000C8BAA /* TableRecordUpdateTests.swift in Sources */,
@@ -3581,6 +3595,7 @@
35813595
5665FA332129EEA0004D8612 /* DatabaseDateEncodingStrategyTests.swift in Sources */,
35823596
563B8FA1249E8ACB007A48C9 /* ValueObservationPrintTests.swift in Sources */,
35833597
56CC9246201E058100CB597E /* DropFirstCursorTests.swift in Sources */,
3598+
4ED4BB592731DD25008B127D /* BackupTestCase.swift in Sources */,
35843599
5698AC401DA2BED90056AF8C /* FTS3PatternTests.swift in Sources */,
35853600
562393571DEE013C00A6B01F /* FilterCursorTests.swift in Sources */,
35863601
5653EAE820944B4F00F46237 /* AssociationChainSQLTests.swift in Sources */,
@@ -3718,6 +3733,7 @@
37183733
AAA4DCA7230F1E0600C74B15 /* TableRecord.swift in Sources */,
37193734
56A2FA3D24424F4800E97D23 /* Export.swift in Sources */,
37203735
AAA4DCA8230F1E0600C74B15 /* Inflections+English.swift in Sources */,
3736+
4E13D2F62769B87F0037588C /* DatabaseBackupProgress.swift in Sources */,
37213737
AAA4DCA9230F1E0600C74B15 /* SQLAssociation.swift in Sources */,
37223738
AAA4DCAA230F1E0600C74B15 /* EncodableRecord.swift in Sources */,
37233739
563B8FAF24A1CE44007A48C9 /* DatabasePublishers.swift in Sources */,
@@ -3949,6 +3965,7 @@
39493965
563B8FA3249E8ACB007A48C9 /* ValueObservationPrintTests.swift in Sources */,
39503966
AAA4DD8A230F262000C74B15 /* RecordPrimaryKeyRowIDTests.swift in Sources */,
39513967
AAA4DD8B230F262000C74B15 /* ForeignKeyInfoTests.swift in Sources */,
3968+
4ED4BB5B2731DD25008B127D /* BackupTestCase.swift in Sources */,
39523969
AAA4DD8C230F262000C74B15 /* AssociationChainSQLTests.swift in Sources */,
39533970
AAA4DD8D230F262000C74B15 /* RecordPrimaryKeyHiddenRowIDTests.swift in Sources */,
39543971
561CFA7A23735016000C8BAA /* TableRecordUpdateTests.swift in Sources */,
@@ -4086,6 +4103,7 @@
40864103
5698AD181DAAD17A0056AF8C /* FTS5Tokenizer.swift in Sources */,
40874104
56A2FA3624424D2A00E97D23 /* Export.swift in Sources */,
40884105
56B964B11DA51D010002DA19 /* FTS5TokenizerDescriptor.swift in Sources */,
4106+
4E13D2F32769B87F0037588C /* DatabaseBackupProgress.swift in Sources */,
40894107
560A37A71C8FF6E500949E71 /* SerializedDatabase.swift in Sources */,
40904108
563B8FAC24A1CE43007A48C9 /* DatabasePublishers.swift in Sources */,
40914109
5605F1691C672E4000235C62 /* NSString.swift in Sources */,

GRDB/Core/Database.swift

Lines changed: 77 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1243,14 +1243,73 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib
12431243

12441244
// MARK: - Backup
12451245

1246-
func backup(
1247-
to dbDest: Database,
1246+
/// Copies the database contents into another database.
1247+
///
1248+
/// The `backup` method blocks the current thread until the destination
1249+
/// database contains the same contents as the source database.
1250+
///
1251+
/// Usage:
1252+
///
1253+
/// let source: DatabaseQueue = ...
1254+
/// let destination: DatabaseQueue = ...
1255+
/// try source.write { sourceDb in
1256+
/// try destination.barrierWriteWithoutTransaction { destDb in
1257+
/// try sourceDb.backup(to: destDb)
1258+
/// }
1259+
/// }
1260+
///
1261+
///
1262+
/// When you're after progress reporting during backup, you'll want to
1263+
/// perform the backup in several steps. Each step copies the number of
1264+
/// _database pages_ you specify. See <https://www.sqlite.org/c3ref/backup_finish.html>
1265+
/// for more information:
1266+
///
1267+
/// // Backup with progress reporting
1268+
/// try sourceDb.backup(
1269+
/// to: destDb,
1270+
/// pagesPerStep: ...)
1271+
/// { backupProgress in
1272+
/// print("Database backup progress:", backupProgress)
1273+
/// }
1274+
///
1275+
/// The `progress` callback will be called at least once—when
1276+
/// `backupProgress.isCompleted == true`. If the callback throws
1277+
/// when `backupProgress.isCompleted == false`, the backup is aborted
1278+
/// and the error is rethrown. If the callback throws when
1279+
/// `backupProgress.isCompleted == true`, backup completion is
1280+
/// unaffected and the error is silently ignored.
1281+
///
1282+
/// See also `DatabaseReader.backup()`.
1283+
///
1284+
/// - parameters:
1285+
/// - destDb: The destination database.
1286+
/// - pagesPerStep: The number of database pages copied on each backup
1287+
/// step. By default, all pages are copied in one single step.
1288+
/// - progress: An optional function that is notified of the backup
1289+
/// progress.
1290+
/// - throws: The error thrown by `progress` if the backup is abandoned, or
1291+
/// any `DatabaseError` that would happen while performing the backup.
1292+
public func backup(
1293+
to destDb: Database,
1294+
pagesPerStep: Int32 = -1,
1295+
progress: ((DatabaseBackupProgress) throws -> ())? = nil)
1296+
throws
1297+
{
1298+
try backupInternal(
1299+
to: destDb,
1300+
pagesPerStep: pagesPerStep,
1301+
afterBackupStep: progress)
1302+
}
1303+
1304+
func backupInternal(
1305+
to destDb: Database,
1306+
pagesPerStep: Int32 = -1,
12481307
afterBackupInit: (() -> Void)? = nil,
1249-
afterBackupStep: (() -> Void)? = nil)
1308+
afterBackupStep: ((DatabaseBackupProgress) throws -> Void)? = nil)
12501309
throws
12511310
{
1252-
guard let backup = sqlite3_backup_init(dbDest.sqliteConnection, "main", sqliteConnection, "main") else {
1253-
throw DatabaseError(resultCode: dbDest.lastErrorCode, message: dbDest.lastErrorMessage)
1311+
guard let backup = sqlite3_backup_init(destDb.sqliteConnection, "main", sqliteConnection, "main") else {
1312+
throw DatabaseError(resultCode: destDb.lastErrorCode, message: destDb.lastErrorMessage)
12541313
}
12551314
guard Int(bitPattern: backup) != Int(SQLITE_ERROR) else {
12561315
throw DatabaseError()
@@ -1260,14 +1319,21 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib
12601319

12611320
do {
12621321
backupLoop: while true {
1263-
switch sqlite3_backup_step(backup, -1) {
1322+
let rc = sqlite3_backup_step(backup, pagesPerStep)
1323+
let totalPageCount = Int(sqlite3_backup_pagecount(backup))
1324+
let remainingPageCount = Int(sqlite3_backup_remaining(backup))
1325+
let progress = DatabaseBackupProgress(
1326+
remainingPageCount: remainingPageCount,
1327+
totalPageCount: totalPageCount,
1328+
isCompleted: rc == SQLITE_DONE)
1329+
switch rc {
12641330
case SQLITE_DONE:
1265-
afterBackupStep?()
1331+
try? afterBackupStep?(progress)
12661332
break backupLoop
12671333
case SQLITE_OK:
1268-
afterBackupStep?()
1334+
try afterBackupStep?(progress)
12691335
case let code:
1270-
throw DatabaseError(resultCode: code, message: dbDest.lastErrorMessage)
1336+
throw DatabaseError(resultCode: code, message: destDb.lastErrorMessage)
12711337
}
12721338
}
12731339
} catch {
@@ -1279,11 +1345,11 @@ public final class Database: CustomStringConvertible, CustomDebugStringConvertib
12791345
case SQLITE_OK:
12801346
break
12811347
case let code:
1282-
throw DatabaseError(resultCode: code, message: dbDest.lastErrorMessage)
1348+
throw DatabaseError(resultCode: code, message: destDb.lastErrorMessage)
12831349
}
12841350

12851351
// The schema of the destination database has changed:
1286-
dbDest.clearSchemaCache()
1352+
destDb.clearSchemaCache()
12871353
}
12881354
}
12891355

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/// An instance of `DatabaseBackupProgress` is passed to a callback of the
2+
/// `DatabaseReader.backup` or `Database.backup` methods to report
3+
/// database backup progress to the caller.
4+
///
5+
/// This is an advanced API for expert users. It is based directly on the SQLite
6+
/// [online backup API](https://www.sqlite.org/c3ref/backup_finish.html).
7+
public struct DatabaseBackupProgress {
8+
/// Total page count is defined by the `sqlite3_backup_remaining` function
9+
public let remainingPageCount: Int
10+
11+
/// Total page count is defined by the `sqlite3_backup_pagecount` function
12+
public let totalPageCount: Int
13+
14+
/// Completed page count is defined as `sqlite3_backup_pagecount() - sqlite3_backup_remaining()`
15+
public var completedPageCount: Int {
16+
totalPageCount - remainingPageCount
17+
}
18+
19+
/// This property is true if and only if `sqlite3_backup_step()` returns
20+
/// `SQLITE_DONE`
21+
public let isCompleted: Bool
22+
}

GRDB/Core/DatabaseReader.swift

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -308,21 +308,68 @@ extension DatabaseReader {
308308
/// When the source is a DatabasePool, concurrent writes can happen during
309309
/// the backup. Those writes may, or may not, be reflected in the backup,
310310
/// but they won't trigger any error.
311-
public func backup(to writer: DatabaseWriter) throws {
312-
try writer.writeWithoutTransaction { dbDest in
313-
try backup(to: dbDest)
311+
///
312+
/// Usage:
313+
///
314+
/// let source: DatabaseQueue = ...
315+
/// let destination: DatabaseQueue = ...
316+
/// try source.backup(to: destination)
317+
///
318+
/// When you're after progress reporting during backup, you'll want to
319+
/// perform the backup in several steps. Each step copies the number of
320+
/// _database pages_ you specify. See <https://www.sqlite.org/c3ref/backup_finish.html>
321+
/// for more information:
322+
///
323+
/// // Backup with progress reporting
324+
/// try source.backup(
325+
/// to: destination,
326+
/// pagesPerStep: ...)
327+
/// { backupProgress in
328+
/// print("Database backup progress:", backupProgress)
329+
/// }
330+
///
331+
/// The `progress` callback will be called at least once—when
332+
/// `backupProgress.isCompleted == true`. If the callback throws
333+
/// when `backupProgress.isCompleted == false`, the backup is aborted
334+
/// and the error is rethrown. If the callback throws when
335+
/// `backupProgress.isCompleted == true`, backup completion is
336+
/// unaffected and the error is silently ignored.
337+
///
338+
/// See also `Database.backup()`.
339+
///
340+
/// - parameters:
341+
/// - writer: The destination database.
342+
/// - pagesPerStep: The number of database pages copied on each backup
343+
/// step. By default, all pages are copied in one single step.
344+
/// - progress: An optional function that is notified of the backup
345+
/// progress.
346+
/// - throws: The error thrown by `progress` if the backup is abandoned, or
347+
/// any `DatabaseError` that would happen while performing the backup.
348+
public func backup(
349+
to writer: DatabaseWriter,
350+
pagesPerStep: Int32 = -1,
351+
progress: ((DatabaseBackupProgress) throws -> ())? = nil)
352+
throws
353+
{
354+
try writer.writeWithoutTransaction { destDb in
355+
try backup(
356+
to: destDb,
357+
pagesPerStep: pagesPerStep,
358+
afterBackupStep: progress)
314359
}
315360
}
316361

317362
func backup(
318-
to dbDest: Database,
363+
to destDb: Database,
364+
pagesPerStep: Int32 = -1,
319365
afterBackupInit: (() -> Void)? = nil,
320-
afterBackupStep: (() -> Void)? = nil)
366+
afterBackupStep: ((DatabaseBackupProgress) throws -> Void)? = nil)
321367
throws
322368
{
323369
try read { dbFrom in
324-
try dbFrom.backup(
325-
to: dbDest,
370+
try dbFrom.backupInternal(
371+
to: destDb,
372+
pagesPerStep: pagesPerStep,
326373
afterBackupInit: afterBackupInit,
327374
afterBackupStep: afterBackupStep)
328375
}

0 commit comments

Comments
 (0)