Skip to content

Commit e7bed4a

Browse files
committed
Merge branch 'backup-sqlite' of https://github.com/kkapitan/SQLite.swift into kkapitan-backup-sqlite
2 parents b94e7a9 + 69ca8fe commit e7bed4a

File tree

4 files changed

+226
-1
lines changed

4 files changed

+226
-1
lines changed

SQLite.xcodeproj/project.pbxproj

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

99
/* Begin PBXBuildFile section */
10+
02A43A9822738CF100FEC494 /* Backup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A43A9722738CF100FEC494 /* Backup.swift */; };
11+
02A43A9922738CF100FEC494 /* Backup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A43A9722738CF100FEC494 /* Backup.swift */; };
12+
02A43A9A22738CF100FEC494 /* Backup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A43A9722738CF100FEC494 /* Backup.swift */; };
13+
02A43A9B22738CF100FEC494 /* Backup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A43A9722738CF100FEC494 /* Backup.swift */; };
1014
03A65E641C6BB0F60062603F /* SQLite.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 03A65E5A1C6BB0F50062603F /* SQLite.framework */; };
1115
03A65E721C6BB2D30062603F /* SQLite.h in Headers */ = {isa = PBXBuildFile; fileRef = EE247AD61C3F04ED00AE3E12 /* SQLite.h */; settings = {ATTRIBUTES = (Public, ); }; };
1216
03A65E731C6BB2D80062603F /* Foundation.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE247AF71C3F06E900AE3E12 /* Foundation.swift */; };
@@ -220,6 +224,7 @@
220224
/* End PBXContainerItemProxy section */
221225

222226
/* Begin PBXFileReference section */
227+
02A43A9722738CF100FEC494 /* Backup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backup.swift; sourceTree = "<group>"; };
223228
03A65E5A1C6BB0F50062603F /* SQLite.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SQLite.framework; sourceTree = BUILT_PRODUCTS_DIR; };
224229
03A65E631C6BB0F60062603F /* SQLiteTests tvOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "SQLiteTests tvOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
225230
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; };
@@ -450,6 +455,7 @@
450455
EE247AF21C3F06E900AE3E12 /* Statement.swift */,
451456
EE247AF31C3F06E900AE3E12 /* Value.swift */,
452457
19A1710E73A46D5AC721CDA9 /* Errors.swift */,
458+
02A43A9722738CF100FEC494 /* Backup.swift */,
453459
);
454460
path = Core;
455461
sourceTree = "<group>";
@@ -834,6 +840,7 @@
834840
03A65E7E1C6BB2FB0062603F /* AggregateFunctions.swift in Sources */,
835841
19A17EC0D68BA8C03288ADF7 /* FTS5.swift in Sources */,
836842
19A179E76EA6207669B60C1B /* Cipher.swift in Sources */,
843+
02A43A9A22738CF100FEC494 /* Backup.swift in Sources */,
837844
19A17FF4A10B44D3937C8CAC /* Errors.swift in Sources */,
838845
19A1737286A74F3CF7412906 /* DateAndTimeFunctions.swift in Sources */,
839846
);
@@ -895,6 +902,7 @@
895902
3D67B3E71DB246BA00A4F4C6 /* Blob.swift in Sources */,
896903
3D67B3E81DB246BA00A4F4C6 /* Connection.swift in Sources */,
897904
19A179CCF9671E345E5A9811 /* Cipher.swift in Sources */,
905+
02A43A9B22738CF100FEC494 /* Backup.swift in Sources */,
898906
19A17DC282E36C4F41AA440B /* Errors.swift in Sources */,
899907
19A173668D948AD4DF1F5352 /* DateAndTimeFunctions.swift in Sources */,
900908
);
@@ -925,6 +933,7 @@
925933
EE247B0D1C3F06E900AE3E12 /* AggregateFunctions.swift in Sources */,
926934
19A1717B10CC941ACB5533D6 /* FTS5.swift in Sources */,
927935
19A171F12AB8B07F2FD7201A /* Cipher.swift in Sources */,
936+
02A43A9822738CF100FEC494 /* Backup.swift in Sources */,
928937
19A1792C0520D4E83C2EB075 /* Errors.swift in Sources */,
929938
19A17E29278A12BC4F542506 /* DateAndTimeFunctions.swift in Sources */,
930939
);
@@ -986,6 +995,7 @@
986995
EE247B6D1C3F3FEC00AE3E12 /* AggregateFunctions.swift in Sources */,
987996
19A1750CEE9B05267995CF3D /* FTS5.swift in Sources */,
988997
19A17835FD5886FDC5A3228F /* Cipher.swift in Sources */,
998+
02A43A9922738CF100FEC494 /* Backup.swift in Sources */,
989999
19A17490543609FCED53CACC /* Errors.swift in Sources */,
9901000
19A17152E32A9585831E3FE0 /* DateAndTimeFunctions.swift in Sources */,
9911001
);

Sources/SQLite/Core/Backup.swift

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
//
2+
// SQLite.swift
3+
// https://github.com/stephencelis/SQLite.swift
4+
// Copyright © 2014-2015 Stephen Celis.
5+
//
6+
// Permission is hereby granted, free of charge, to any person obtaining a copy
7+
// of this software and associated documentation files (the "Software"), to deal
8+
// in the Software without restriction, including without limitation the rights
9+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
// copies of the Software, and to permit persons to whom the Software is
11+
// furnished to do so, subject to the following conditions:
12+
//
13+
// The above copyright notice and this permission notice shall be included in
14+
// all copies or substantial portions of the Software.
15+
//
16+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
// THE SOFTWARE.
23+
//
24+
25+
import Foundation
26+
import Dispatch
27+
#if SQLITE_SWIFT_STANDALONE
28+
import sqlite3
29+
#elseif SQLITE_SWIFT_SQLCIPHER
30+
import SQLCipher
31+
#elseif os(Linux)
32+
import CSQLite
33+
#else
34+
import SQLite3
35+
#endif
36+
37+
/// An object representing database backup.
38+
///
39+
/// See: <https://www.sqlite.org/backup.html>
40+
public final class Backup {
41+
42+
/// The name of the database to backup
43+
public enum DatabaseName {
44+
45+
/// The main database
46+
case main
47+
48+
/// The temporary database
49+
case temp
50+
51+
/// A database added to the connection with ATTACH statement
52+
case attached(name: String)
53+
54+
var name: String {
55+
switch self {
56+
case .main:
57+
return "main"
58+
case .temp:
59+
return "temp"
60+
case .attached(let name):
61+
return name
62+
}
63+
}
64+
}
65+
66+
/// Number of pages to copy while performing a backup step
67+
public enum Pages {
68+
69+
/// Indicates all remaining pages should be copied
70+
case all
71+
72+
/// Indicates the maximal number of pages to be copied in single step
73+
case limited(number: Int32)
74+
75+
var number: Int32 {
76+
switch self {
77+
case .all:
78+
return -1
79+
case .limited(let number):
80+
return number
81+
}
82+
}
83+
}
84+
85+
/// Total number of pages to copy
86+
///
87+
/// See: <https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backuppagecount>
88+
public var pageCount: Int32 {
89+
return handle.map { sqlite3_backup_pagecount($0) } ?? 0
90+
}
91+
92+
/// Number of remaining pages to copy.
93+
///
94+
/// See: <https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backupremaining>
95+
public var remainingPages: Int32 {
96+
return handle.map { sqlite3_backup_remaining($0) } ?? 0
97+
}
98+
99+
private let targetConnection: Connection
100+
private let sourceConnection: Connection
101+
102+
private var handle: OpaquePointer?
103+
104+
/// Initializes a new SQLite backup.
105+
///
106+
/// - Parameters:
107+
///
108+
/// - targetConnection: The connection to the database to save backup into.
109+
///
110+
/// - targetName: The name of the database to save backup into.
111+
///
112+
/// Default: `.main`.
113+
///
114+
/// - sourceConnection: The connection to the database to backup.
115+
///
116+
/// - sourceName: The name of the database to backup.
117+
///
118+
/// Default: `.main`.
119+
///
120+
/// - Returns: A new database backup.
121+
///
122+
/// See: <https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backupinit>
123+
public init(targetConnection: Connection,
124+
targetName: DatabaseName = .main,
125+
sourceConnection: Connection,
126+
sourceName: DatabaseName = .main) throws {
127+
128+
self.targetConnection = targetConnection
129+
self.sourceConnection = sourceConnection
130+
131+
self.handle = sqlite3_backup_init(targetConnection.handle,
132+
targetName.name,
133+
sourceConnection.handle,
134+
sourceName.name)
135+
136+
if self.handle == nil, let error = Result(errorCode: sqlite3_errcode(targetConnection.handle), connection: targetConnection) {
137+
throw error
138+
}
139+
}
140+
141+
/// Performs a backup step.
142+
///
143+
/// - Parameter pagesToCopy: The maximal number of pages to copy in one step
144+
///
145+
/// - Throws: `Result.Error` if step fails.
146+
//
147+
/// See: <https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backupstep>
148+
public func step(pagesToCopy pages: Pages = .all) throws {
149+
let status = sqlite3_backup_step(handle, pages.number)
150+
151+
guard status != SQLITE_DONE else {
152+
finish()
153+
return
154+
}
155+
156+
if let error = Result(errorCode: status, connection: targetConnection) {
157+
throw error
158+
}
159+
}
160+
161+
/// Finalizes backup.
162+
///
163+
/// See: <https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backupfinish>
164+
public func finish() {
165+
guard let handle = self.handle else {
166+
return
167+
}
168+
169+
sqlite3_backup_finish(handle)
170+
self.handle = nil
171+
}
172+
173+
deinit {
174+
finish()
175+
}
176+
}

Sources/SQLite/Core/Connection.swift

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -790,7 +790,34 @@ public final class Connection {
790790
}
791791
fileprivate typealias Collation = @convention(block) (UnsafeRawPointer, UnsafeRawPointer) -> Int32
792792
fileprivate var collations = [String: Collation]()
793-
793+
794+
// MARK: - Backup
795+
796+
/// Prepares a new backup for current connection.
797+
///
798+
/// - Parameters:
799+
///
800+
/// - databaseName: The name of the database to backup.
801+
///
802+
/// Default: `.main`
803+
///
804+
/// - targetConnection: The name of the database to save backup into.
805+
///
806+
/// - targetDatabaseName: The name of the database to save backup into.
807+
///
808+
/// Default: `.main`.
809+
///
810+
/// - Returns: A new database backup.
811+
812+
public func backup(databaseName: Backup.DatabaseName = .main,
813+
usingConnection targetConnection: Connection,
814+
andDatabaseName targetDatabaseName: Backup.DatabaseName = .main) throws -> Backup {
815+
return try Backup(targetConnection: targetConnection,
816+
targetName: targetDatabaseName,
817+
sourceConnection: self,
818+
sourceName: databaseName)
819+
}
820+
794821
// MARK: - Error Handling
795822

796823
func sync<T>(_ block: () throws -> T) rethrows -> T {

Tests/SQLiteTests/ConnectionTests.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,18 @@ class ConnectionTests: SQLiteTestCase {
147147

148148
assertSQL("BEGIN EXCLUSIVE TRANSACTION")
149149
}
150+
151+
func test_backup_copiesDatabase() throws {
152+
let target = try Connection()
153+
154+
try InsertUsers("alice", "betsy")
155+
156+
let backup = try db.backup(usingConnection: target)
157+
try backup.step()
158+
159+
let users = try target.prepare("SELECT email FROM users ORDER BY email")
160+
XCTAssertEqual(users.map { $0[0] as? String }, ["alice@example.com", "betsy@example.com"])
161+
}
150162

151163
func test_transaction_beginsAndCommitsTransactions() {
152164
let stmt = try! db.prepare("INSERT INTO users (email) VALUES (?)", "alice@example.com")

0 commit comments

Comments
 (0)