From c5fa7d742876c70390ba9f664f918b9f180c0306 Mon Sep 17 00:00:00 2001
From: Krzysztof <krzysztof.kapitan@gmail.com>
Date: Mon, 29 Apr 2019 18:07:45 +0200
Subject: [PATCH] More convenient API for backup

---
 SQLite.xcodeproj/project.pbxproj        |   5 +
 Sources/SQLite/Core/Backup.swift        | 176 ++++++++++++++++++++++++
 Sources/SQLite/Core/Connection.swift    |  29 +++-
 Tests/SQLiteTests/ConnectionTests.swift |  12 ++
 4 files changed, 221 insertions(+), 1 deletion(-)
 create mode 100644 Sources/SQLite/Core/Backup.swift

diff --git a/SQLite.xcodeproj/project.pbxproj b/SQLite.xcodeproj/project.pbxproj
index a730284d..dde85c87 100644
--- a/SQLite.xcodeproj/project.pbxproj
+++ b/SQLite.xcodeproj/project.pbxproj
@@ -7,6 +7,10 @@
 	objects = {
 
 /* Begin PBXBuildFile section */
+		02A43A9822738CF100FEC494 /* Backup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A43A9722738CF100FEC494 /* Backup.swift */; };
+		02A43A9922738CF100FEC494 /* Backup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A43A9722738CF100FEC494 /* Backup.swift */; };
+		02A43A9A22738CF100FEC494 /* Backup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A43A9722738CF100FEC494 /* Backup.swift */; };
+		02A43A9B22738CF100FEC494 /* Backup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A43A9722738CF100FEC494 /* Backup.swift */; };
 		03A65E641C6BB0F60062603F /* SQLite.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 03A65E5A1C6BB0F50062603F /* SQLite.framework */; };
 		03A65E721C6BB2D30062603F /* SQLite.h in Headers */ = {isa = PBXBuildFile; fileRef = EE247AD61C3F04ED00AE3E12 /* SQLite.h */; settings = {ATTRIBUTES = (Public, ); }; };
 		03A65E731C6BB2D80062603F /* Foundation.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF71C3F06E900AE3E12 /* Foundation.swift */; };
@@ -214,6 +218,7 @@
 /* End PBXContainerItemProxy section */
 
 /* Begin PBXFileReference section */
+		02A43A9722738CF100FEC494 /* Backup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backup.swift; sourceTree = "<group>"; };
 		03A65E5A1C6BB0F50062603F /* SQLite.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SQLite.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		03A65E631C6BB0F60062603F /* SQLiteTests tvOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "SQLiteTests tvOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
 		03A65E961C6BB3210062603F /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.tbd; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS.sdk/usr/lib/libsqlite3.tbd; sourceTree = DEVELOPER_DIR; };
diff --git a/Sources/SQLite/Core/Backup.swift b/Sources/SQLite/Core/Backup.swift
new file mode 100644
index 00000000..1ea9637d
--- /dev/null
+++ b/Sources/SQLite/Core/Backup.swift
@@ -0,0 +1,176 @@
+//
+// SQLite.swift
+// https://github.com/stephencelis/SQLite.swift
+// Copyright © 2014-2015 Stephen Celis.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+import Foundation
+import Dispatch
+#if SQLITE_SWIFT_STANDALONE
+import sqlite3
+#elseif SQLITE_SWIFT_SQLCIPHER
+import SQLCipher
+#elseif os(Linux)
+import CSQLite
+#else
+import SQLite3
+#endif
+
+/// An object representing database backup.
+///
+/// See: <https://www.sqlite.org/backup.html>
+public final class Backup {
+    
+    /// The name of the database to backup
+    public enum DatabaseName {
+        
+        /// The main database
+        case main
+        
+        /// The temporary database
+        case temp
+        
+        /// A database added to the connection with ATTACH statement
+        case attached(name: String)
+        
+        var name: String {
+            switch self {
+            case .main:
+                return "main"
+            case .temp:
+                return "temp"
+            case .attached(let name):
+                return name
+            }
+        }
+    }
+    
+    /// Number of pages to copy while performing a backup step
+    public enum Pages {
+        
+        /// Indicates all remaining pages should be copied
+        case all
+        
+        /// Indicates the maximal number of pages to be copied in single step
+        case limited(number: Int32)
+        
+        var number: Int32 {
+            switch self {
+            case .all:
+                return -1
+            case .limited(let number):
+                return number
+            }
+        }
+    }
+    
+    /// Total number of pages to copy
+    ///
+    /// See: <https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backuppagecount>
+    public var pageCount: Int32 {
+        return handle.map { sqlite3_backup_pagecount($0) } ?? 0
+    }
+    
+    /// Number of remaining pages to copy.
+    ///
+    /// See: <https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backupremaining>
+    public var remainingPages: Int32 {
+        return handle.map { sqlite3_backup_remaining($0) } ?? 0
+    }
+    
+    private let targetConnection: Connection
+    private let sourceConnection: Connection
+    
+    private var handle: OpaquePointer?
+    
+    /// Initializes a new SQLite backup.
+    ///
+    /// - Parameters:
+    ///
+    ///   - targetConnection: The connection to the database to save backup into.
+    ///
+    ///   - targetName: The name of the database to save backup into.
+    ///
+    ///     Default: `.main`.
+    ///
+    ///   - sourceConnection: The connection to the database to backup.
+    ///
+    ///   - sourceName: The name of the database to backup.
+    /// 
+    ///     Default: `.main`.
+    ///
+    /// - Returns: A new database backup.
+    ///
+    /// See: <https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backupinit>
+    public init(targetConnection: Connection,
+                targetName: DatabaseName = .main,
+                sourceConnection: Connection,
+                sourceName: DatabaseName = .main) throws {
+        
+        self.targetConnection = targetConnection
+        self.sourceConnection = sourceConnection
+        
+        self.handle = sqlite3_backup_init(targetConnection.handle,
+                                          targetName.name,
+                                          sourceConnection.handle,
+                                          sourceName.name)
+        
+        if self.handle == nil, let error = Result(errorCode: sqlite3_errcode(targetConnection.handle), connection: targetConnection) {
+            throw error
+        }
+    }
+    
+    /// Performs a backup step.
+    ///
+    /// - Parameter pagesToCopy: The maximal number of pages to copy in one step
+    ///
+    /// - Throws: `Result.Error` if step fails.
+    //
+    /// See: <https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backupstep>
+    public func step(pagesToCopy pages: Pages = .all) throws {
+        let status = sqlite3_backup_step(handle, pages.number)
+        
+        guard status != SQLITE_DONE else {
+            finish()
+            return
+        }
+        
+        if let error = Result(errorCode: status, connection: targetConnection) {
+            throw error
+        }
+    }
+    
+    /// Finalizes backup.
+    ///
+    /// See: <https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backupfinish>
+    public func finish() {
+        guard let handle = self.handle else {
+            return
+        }
+        
+        sqlite3_backup_finish(handle)
+        self.handle = nil
+    }
+    
+    deinit {
+        finish()
+    }
+}
diff --git a/Sources/SQLite/Core/Connection.swift b/Sources/SQLite/Core/Connection.swift
index 1bbf7f73..c23076af 100644
--- a/Sources/SQLite/Core/Connection.swift
+++ b/Sources/SQLite/Core/Connection.swift
@@ -627,7 +627,34 @@ public final class Connection {
     }
     fileprivate typealias Collation = @convention(block) (UnsafeRawPointer, UnsafeRawPointer) -> Int32
     fileprivate var collations = [String: Collation]()
-
+    
+    // MARK: - Backup
+    
+    /// Prepares a new backup for current connection.
+    ///
+    /// - Parameters:
+    ///
+    ///   - databaseName: The name of the database to backup.
+    ///
+    ///     Default: `.main`
+    ///
+    ///   - targetConnection: The name of the database to save backup into.
+    ///
+    ///   - targetDatabaseName: The name of the database to save backup into.
+    ///
+    ///     Default: `.main`.
+    ///
+    /// - Returns: A new database backup.
+    
+    public func backup(databaseName: Backup.DatabaseName = .main,
+                       usingConnection targetConnection: Connection,
+                       andDatabaseName targetDatabaseName: Backup.DatabaseName = .main) throws -> Backup {
+        return try Backup(targetConnection: targetConnection,
+                          targetName: targetDatabaseName,
+                          sourceConnection: self,
+                          sourceName: databaseName)
+    }
+    
     // MARK: - Error Handling
 
     func sync<T>(_ block: () throws -> T) rethrows -> T {
diff --git a/Tests/SQLiteTests/ConnectionTests.swift b/Tests/SQLiteTests/ConnectionTests.swift
index fdfc9637..5829a9f3 100644
--- a/Tests/SQLiteTests/ConnectionTests.swift
+++ b/Tests/SQLiteTests/ConnectionTests.swift
@@ -143,6 +143,18 @@ class ConnectionTests : SQLiteTestCase {
 
         AssertSQL("BEGIN EXCLUSIVE TRANSACTION")
     }
+    
+    func test_backup_copiesDatabase() throws {
+        let target = try Connection()
+        
+        try InsertUsers("alice", "betsy")
+        
+        let backup = try db.backup(usingConnection: target)
+        try backup.step()
+        
+        let users = try target.prepare("SELECT email FROM users ORDER BY email")
+        XCTAssertEqual(users.map { $0[0] as? String }, ["alice@example.com", "betsy@example.com"])
+    }
 
     func test_transaction_beginsAndCommitsTransactions() {
         let stmt = try! db.prepare("INSERT INTO users (email) VALUES (?)", "alice@example.com")
