Skip to content

Commit 36e30a6

Browse files
committed
Merge branch 'development'
2 parents aa0079a + 368514c commit 36e30a6

83 files changed

Lines changed: 1378 additions & 440 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.vscode/settings.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"editor.formatOnSave": false,
3+
"editor.insertSpaces": true,
4+
"editor.tabSize": 4,
5+
"editor.detectIndentation": false,
6+
"editor.trimAutoWhitespace": false
7+
}

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one exception:
77

88
#### 7.x Releases
99

10+
- `7.10.x` Releases - [7.10.0](#7100)
1011
- `7.9.x` Releases - [7.9.0](#790)
1112
- `7.8.x` Releases - [7.8.0](#780)
1213
- `7.7.x` Releases - [7.7.0](#770) - [7.7.1](#771)
@@ -141,6 +142,16 @@ GRDB adheres to [Semantic Versioning](https://semver.org/), with one exception:
141142

142143
---
143144

145+
## 7.10.0
146+
147+
Released February 15, 2025
148+
149+
- **Documentation fixes** by [@bellebethcooper](https://github.com/bellebethcooper), [@Cykelero](https://github.com/Cykelero), and [@leejungyeob](https://github.com/leejungyeob) in [#1842](https://github.com/groue/GRDB.swift/pull/1842), [#1846](https://github.com/groue/GRDB.swift/pull/1846), [#1848](https://github.com/groue/GRDB.swift/pull/1848)
150+
- **New**: Linux adjustments by [@thinkpractice](https://github.com/thinkpractice) in [#1825](https://github.com/groue/GRDB.swift/pull/1825)
151+
- **New**: SQLCipher adjustments by [@R4N](https://github.com/R4N) in [#1845](https://github.com/groue/GRDB.swift/pull/1845)
152+
- **New**: Android and Windows adjustments by [@marcprux](https://github.com/marcprux) in [#1849](https://github.com/groue/GRDB.swift/pull/1849), [#1850](https://github.com/groue/GRDB.swift/pull/1850)
153+
- **New**: Configure whether UPSERT should update all columns, or no column by [@groue](https://github.com/groue) in [#1852](https://github.com/groue/GRDB.swift/pull/1852)
154+
144155
## 7.9.0
145156

146157
Released December 13, 2025

GRDB.swift.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Pod::Spec.new do |s|
22
s.name = 'GRDB.swift'
3-
s.version = '7.9.0'
3+
s.version = '7.10.0'
44

55
s.license = { :type => 'MIT', :file => 'LICENSE' }
66
s.summary = 'A toolkit for SQLite databases, with a focus on application development.'

GRDB/Core/Configuration.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import SQLCipher
44
#elseif GRDBFRAMEWORK // GRDB.xcodeproj or CocoaPods (standard subspec)
55
import SQLite3
66
#elseif GRDBCUSTOMSQLITE // GRDBCustom Framework
7-
// #elseif SomeTrait
8-
// import ...
7+
#elseif SQLCipher
8+
import SQLCipher
99
#else // Default SPM trait must be the default. It impossible to detect from Xcode.
1010
import GRDBSQLite
1111
#endif

GRDB/Core/Database+SQLCipher.swift

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
#if SQLITE_HAS_CODEC
2+
#if GRDBCIPHER // CocoaPods (SQLCipher subspec)
3+
import SQLCipher
4+
#elseif SQLCipher
5+
import SQLCipher
6+
#else
7+
#error("No SQLCipher library configured")
8+
#endif
9+
import Foundation
10+
11+
extension Database {
12+
13+
/// Destination of SQLCipher logs.
14+
///
15+
/// See <https://www.zetetic.net/sqlcipher/sqlcipher-api/#cipher_log>
16+
public struct SQLCipherLogTarget: Sendable {
17+
public var rawValue: String
18+
public init(rawValue: String) {
19+
self.rawValue = rawValue
20+
}
21+
22+
public static let stdout = Self(rawValue: "stdout")
23+
public static let stderr = Self(rawValue: "stderr")
24+
public static let device = Self(rawValue: "device")
25+
public static func file(_ path: String) -> Self { Self(rawValue: path) }
26+
}
27+
28+
/// Granularitly of SQLCipher log outputs.
29+
///
30+
/// Each log level is more verbose than the last. With ``debug`` and
31+
/// ``trace`` the logging system will generate a significant log volume.
32+
///
33+
/// See <https://www.zetetic.net/sqlcipher/sqlcipher-api/#cipher_log_level>
34+
///
35+
/// ## Topics
36+
///
37+
/// ### Log Levels
38+
///
39+
/// - ``none``
40+
/// - ``error``
41+
/// - ``warn``
42+
/// - ``info``
43+
/// - ``debug``
44+
/// - ``trace``
45+
public struct SQLCipherLogLevel: RawRepresentable, Sendable {
46+
public var rawValue: String
47+
public init(rawValue: String) {
48+
self.rawValue = rawValue
49+
}
50+
51+
public static let none = Self(rawValue: "NONE")
52+
public static let error = Self(rawValue: "ERROR")
53+
public static let warn = Self(rawValue: "WARN")
54+
public static let info = Self(rawValue: "INFO")
55+
public static let debug = Self(rawValue: "DEBUG")
56+
public static let trace = Self(rawValue: "TRACE")
57+
}
58+
59+
/// - Returns: the SQLCipher version
60+
///
61+
/// See <https://www.zetetic.net/sqlcipher/sqlcipher-api/#cipher_version>
62+
public var cipherVersion: String {
63+
get throws { try String.fetchOne(self, sql: "PRAGMA cipher_version")! }
64+
}
65+
66+
/// - Returns: the SQLCipher fips status: 1 for fips mode, 0 for non-fips mode
67+
/// The FIPS status will not be initialized until the database connection has been keyed
68+
///
69+
/// See <https://www.zetetic.net/sqlcipher/sqlcipher-api/#cipher_fips_status>
70+
public var cipherFipsStatus: String? {
71+
get throws { try String.fetchOne(self, sql: "PRAGMA cipher_fips_status") }
72+
}
73+
74+
/// - Returns: The compiled crypto provider.
75+
/// The database must be keyed before requesting the name of the crypto provider.
76+
///
77+
/// See <https://www.zetetic.net/sqlcipher/sqlcipher-api/#cipher_provider>
78+
public var cipherProvider: String? {
79+
get throws { try String.fetchOne(self, sql: "PRAGMA cipher_provider") }
80+
}
81+
82+
/// - Returns: the version number provided from the compiled crypto provider.
83+
/// This value, if known, is available only after the database has been keyed.
84+
///
85+
/// See <https://www.zetetic.net/sqlcipher/sqlcipher-api/#cipher_provider_version>
86+
public var cipherProviderVersion: String? {
87+
get throws { try String.fetchOne(self, sql: "PRAGMA cipher_provider_version") }
88+
}
89+
90+
/// Sets the passphrase used to crypt and decrypt an SQLCipher database.
91+
///
92+
/// Call this method from `Configuration.prepareDatabase`,
93+
/// as in the example below:
94+
///
95+
/// var config = Configuration()
96+
/// config.prepareDatabase { db in
97+
/// try db.usePassphrase("secret")
98+
/// }
99+
public func usePassphrase(_ passphrase: String) throws {
100+
guard var data = passphrase.data(using: .utf8) else {
101+
throw DatabaseError(message: "invalid passphrase")
102+
}
103+
defer {
104+
data.resetBytes(in: 0..<data.count)
105+
}
106+
try usePassphrase(data)
107+
}
108+
109+
/// Sets the passphrase used to crypt and decrypt an SQLCipher database.
110+
///
111+
/// Call this method from `Configuration.prepareDatabase`,
112+
/// as in the example below:
113+
///
114+
/// var config = Configuration()
115+
/// config.prepareDatabase { db in
116+
/// try db.usePassphrase(passphraseData)
117+
/// }
118+
public func usePassphrase(_ passphrase: Data) throws {
119+
let code = passphrase.withUnsafeBytes {
120+
sqlite3_key(sqliteConnection, $0.baseAddress, CInt($0.count))
121+
}
122+
guard code == SQLITE_OK else {
123+
throw DatabaseError(resultCode: code, message: String(cString: sqlite3_errmsg(sqliteConnection)))
124+
}
125+
}
126+
127+
/// Changes the passphrase used by an SQLCipher encrypted database.
128+
public func changePassphrase(_ passphrase: String) throws {
129+
guard var data = passphrase.data(using: .utf8) else {
130+
throw DatabaseError(message: "invalid passphrase")
131+
}
132+
defer {
133+
data.resetBytes(in: 0..<data.count)
134+
}
135+
try changePassphrase(data)
136+
}
137+
138+
/// Changes the passphrase used by an SQLCipher encrypted database.
139+
public func changePassphrase(_ passphrase: Data) throws {
140+
// FIXME: sqlite3_rekey is discouraged.
141+
//
142+
// https://github.com/ccgus/fmdb/issues/547#issuecomment-259219320
143+
//
144+
// > We (Zetetic) have been discouraging the use of sqlite3_rekey in
145+
// > favor of attaching a new database with the desired encryption
146+
// > options and using sqlcipher_export() to migrate the contents and
147+
// > schema of the original db into the new one:
148+
// > https://discuss.zetetic.net/t/how-to-encrypt-a-plaintext-sqlite-database-to-use-sqlcipher-and-avoid-file-is-encrypted-or-is-not-a-database-errors/
149+
let code = passphrase.withUnsafeBytes {
150+
sqlite3_rekey(sqliteConnection, $0.baseAddress, CInt($0.count))
151+
}
152+
guard code == SQLITE_OK else {
153+
throw DatabaseError(resultCode: code, message: lastErrorMessage)
154+
}
155+
}
156+
157+
/// When using Commercial or Enterprise SQLCipher packages you must call
158+
/// `PRAGMA cipher_license` with a valid license code prior to executing
159+
/// cryptographic operations on an encrypted database.
160+
/// Failure to provide a license code, or use of an expired trial code,
161+
/// will result in an `SQLITE_AUTH (23)` error code reported from the SQLite API
162+
/// License Codes will activate SQLCipher Commercial or Enterprise packages
163+
/// from Zetetic: <https://www.zetetic.net/sqlcipher/buy/>
164+
/// 15-day free trials are available by request: <https://www.zetetic.net/sqlcipher/trial/>
165+
///
166+
/// Call this method from ``Configuration/prepareDatabase(_:)``,
167+
/// as in the example below:
168+
///
169+
/// ```swift
170+
/// var config = Configuration()
171+
/// config.prepareDatabase { db in
172+
/// try db.applySQLCipherLicense(license)
173+
/// }
174+
/// ```
175+
///
176+
/// See <https://www.zetetic.net/sqlcipher/sqlcipher-api/#cipher_license>
177+
/// - Parameter license: base64 SQLCipher license code to activate SQLCipher commercial
178+
public func applySQLCipherLicense(_ license: String) throws {
179+
try execute(sql: "PRAGMA cipher_license = '\(license)'")
180+
}
181+
182+
/// Instructs SQLCipher to log internal debugging and operational information.
183+
///
184+
/// The supplied ``logLevel`` determines the granularity of the logs output.
185+
///
186+
/// See <https://www.zetetic.net/sqlcipher/sqlcipher-api/#cipher_log>
187+
/// - Parameter logLevel: The granularity to use for the logging system - defaults to `DEBUG`.
188+
/// - Parameter target: The destination of SQLCipher logs - defaults to `.device`.
189+
public func enableCipherLogging(
190+
logLevel: SQLCipherLogLevel = .debug,
191+
target: SQLCipherLogTarget = .device
192+
) throws {
193+
// Pragma do not support SQL arguments. We need to generate SQL that contains literal values:
194+
// PRAGMA cipher_log = '/path/to/file'
195+
// PRAGMA cipher_log_level = 'xxx'
196+
let context = SQLGenerationContext(self, argumentsSink: .literalValues)
197+
try execute(sql: SQL("PRAGMA cipher_log = \(target.rawValue)").sql(context))
198+
try execute(sql: SQL("PRAGMA cipher_log_level = \(logLevel.rawValue.uppercased())").sql(context))
199+
}
200+
201+
/// Instructs SQLCipher to disable logging internal debugging and operational information.
202+
///
203+
/// See <https://www.zetetic.net/sqlcipher/sqlcipher-api/#cipher_log>
204+
public func disableCipherLogging() throws {
205+
try execute(sql: "PRAGMA cipher_log_level = NONE")
206+
}
207+
208+
internal func validateSQLCipher() throws {
209+
// https://discuss.zetetic.net/t/important-advisory-sqlcipher-with-xcode-8-and-new-sdks/1688
210+
//
211+
// > In order to avoid situations where SQLite might be used
212+
// > improperly at runtime, we strongly recommend that
213+
// > applications institute a runtime test to ensure that the
214+
// > application is actually using SQLCipher on the active
215+
// > connection.
216+
if try String.fetchOne(self, sql: "PRAGMA cipher_version") == nil {
217+
throw DatabaseError(resultCode: .SQLITE_MISUSE, message: """
218+
GRDB is not linked against SQLCipher. \
219+
Check https://discuss.zetetic.net/t/important-advisory-sqlcipher-with-xcode-8-and-new-sdks/1688
220+
""")
221+
}
222+
}
223+
224+
internal func dropAllDatabaseObjects() throws {
225+
// SQLCipher does not support the backup API:
226+
// https://discuss.zetetic.net/t/using-the-sqlite-online-backup-api/2631
227+
// So we'll drop all database objects one after the other.
228+
229+
// Prevent foreign keys from messing with drop table statements
230+
let foreignKeysEnabled = try Bool.fetchOne(self, sql: "PRAGMA foreign_keys")!
231+
if foreignKeysEnabled {
232+
try execute(sql: "PRAGMA foreign_keys = OFF")
233+
}
234+
235+
try throwingFirstError(
236+
execute: {
237+
// Remove all database objects, one after the other
238+
try inTransaction {
239+
let sql = "SELECT type, name FROM sqlite_master WHERE name NOT LIKE 'sqlite_%'"
240+
while let row = try Row.fetchOne(self, sql: sql) {
241+
let type: String = row["type"]
242+
let name: String = row["name"]
243+
try execute(sql: "DROP \(type) \(name.quotedDatabaseIdentifier)")
244+
}
245+
return .commit
246+
}
247+
},
248+
finally: {
249+
// Restore foreign keys if needed
250+
if foreignKeysEnabled {
251+
try execute(sql: "PRAGMA foreign_keys = ON")
252+
}
253+
})
254+
}
255+
}
256+
257+
#endif

GRDB/Core/Database+Schema.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import SQLCipher
44
#elseif GRDBFRAMEWORK // GRDB.xcodeproj or CocoaPods (standard subspec)
55
import SQLite3
66
#elseif GRDBCUSTOMSQLITE // GRDBCustom Framework
7-
// #elseif SomeTrait
8-
// import ...
7+
#elseif SQLCipher
8+
import SQLCipher
99
#else // Default SPM trait must be the default. It impossible to detect from Xcode.
1010
import GRDBSQLite
1111
#endif

GRDB/Core/Database+Statements.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import SQLCipher
44
#elseif GRDBFRAMEWORK // GRDB.xcodeproj or CocoaPods (standard subspec)
55
import SQLite3
66
#elseif GRDBCUSTOMSQLITE // GRDBCustom Framework
7-
// #elseif SomeTrait
8-
// import ...
7+
#elseif SQLCipher
8+
import SQLCipher
99
#else // Default SPM trait must be the default. It impossible to detect from Xcode.
1010
import GRDBSQLite
1111
#endif

0 commit comments

Comments
 (0)