Skip to content

Commit bc79617

Browse files
authored
[5.4] add configuration to limit size of sqlite (#181)
motivation: there are use cases where users of the SQLite DB may want to limit its size changes: * introduce SQLiteCache::Configuraiton allowing to set max database size * refactor SQLite::checkError to throw more structured errro when database is full, adding infrastructure to other structured errors * add tests * Update CODEOWNERS (#180)
1 parent 2f16773 commit bc79617

File tree

3 files changed

+72
-15
lines changed

3 files changed

+72
-15
lines changed

CODEOWNERS

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,4 @@
2424

2525
# The following lines are used by GitHub to automatically recommend reviewers.
2626

27-
* @aciidb0mb3r @abertelrud @neonichu @friedbunny
27+
* @aciidb0mb3r @abertelrud @neonichu @friedbunny @tomerd

Sources/TSCUtility/SQLite.swift

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,21 +32,25 @@ public struct SQLite {
3232
self.configuration = configuration
3333

3434
var handle: OpaquePointer?
35-
try Self.checkError("Unable to open database at \(self.location)") {
35+
try Self.checkError ({
3636
sqlite3_open_v2(
3737
location.pathString,
3838
&handle,
3939
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX,
4040
nil
4141
)
42-
}
42+
},
43+
description: "Unable to open database at \(self.location)")
4344

4445
guard let db = handle else {
4546
throw StringError("Unable to open database at \(self.location)")
4647
}
4748
self.db = db
48-
try Self.checkError("Unable to configure database") { sqlite3_extended_result_codes(db, 1) }
49-
try Self.checkError("Unable to configure database") { sqlite3_busy_timeout(db, self.configuration.busyTimeoutMilliseconds) }
49+
try Self.checkError({ sqlite3_extended_result_codes(db, 1) }, description: "Unable to configure database")
50+
try Self.checkError({ sqlite3_busy_timeout(db, self.configuration.busyTimeoutMilliseconds) }, description: "Unable to configure database busy timeout")
51+
if let maxPageCount = self.configuration.maxPageCount {
52+
try self.exec(query: "PRAGMA max_page_count=\(maxPageCount);")
53+
}
5054
}
5155

5256
@available(*, deprecated, message: "use init(location:configuration) instead")
@@ -90,9 +94,14 @@ public struct SQLite {
9094

9195
public struct Configuration {
9296
public var busyTimeoutMilliseconds: Int32
97+
public var maxSizeInBytes: Int?
98+
99+
// https://www.sqlite.org/pgszchng2016.html
100+
private let defaultPageSizeInBytes = 1024
93101

94102
public init() {
95103
self.busyTimeoutMilliseconds = 5000
104+
self.maxSizeInBytes = .none
96105
}
97106

98107
// FIXME: deprecated 12/2020, remove once clients migrated over
@@ -113,6 +122,19 @@ public struct SQLite {
113122
self.busyTimeoutMilliseconds = newValue * 1000
114123
}
115124
}
125+
126+
public var maxSizeInMegabytes: Int? {
127+
get {
128+
self.maxSizeInBytes.map { $0 / (1024 * 1024) }
129+
}
130+
set {
131+
self.maxSizeInBytes = newValue.map { $0 * 1024 * 1024 }
132+
}
133+
}
134+
135+
public var maxPageCount: Int? {
136+
self.maxSizeInBytes.map { $0 / self.defaultPageSizeInBytes }
137+
}
116138
}
117139

118140
public enum Location {
@@ -179,7 +201,7 @@ public struct SQLite {
179201

180202
public init(db: OpaquePointer, query: String) throws {
181203
var stmt: OpaquePointer?
182-
try checkError { sqlite3_prepare_v2(db, query, -1, &stmt, nil) }
204+
try SQLite.checkError { sqlite3_prepare_v2(db, query, -1, &stmt, nil) }
183205
self.stmt = stmt!
184206
}
185207

@@ -227,17 +249,17 @@ public struct SQLite {
227249

228250
/// Reset the prepared statement.
229251
public func reset() throws {
230-
try checkError { sqlite3_reset(stmt) }
252+
try SQLite.checkError { sqlite3_reset(stmt) }
231253
}
232254

233255
/// Clear bindings from the prepared statment.
234256
public func clearBindings() throws {
235-
try checkError { sqlite3_clear_bindings(stmt) }
257+
try SQLite.checkError { sqlite3_clear_bindings(stmt) }
236258
}
237259

238260
/// Finalize the statement and free up resources.
239261
public func finalize() throws {
240-
try checkError { sqlite3_finalize(stmt) }
262+
try SQLite.checkError { sqlite3_finalize(stmt) }
241263
}
242264
}
243265

@@ -248,17 +270,25 @@ public struct SQLite {
248270
}
249271
}
250272

251-
private static func checkError(_ errorPrefix: String? = nil, _ fn: () -> Int32) throws {
273+
private static func checkError(_ fn: () -> Int32, description prefix: String? = .none) throws {
252274
let result = fn()
253275
if result != SQLITE_OK {
254-
var error = ""
255-
if let errorPrefix = errorPrefix {
256-
error += errorPrefix + ": "
276+
var description = String(cString: sqlite3_errstr(result))
277+
switch description.lowercased() {
278+
case "database or disk is full":
279+
throw Errors.databaseFull
280+
default:
281+
if let prefix = prefix {
282+
description = "\(prefix): \(description)"
283+
}
284+
throw StringError(description)
257285
}
258-
error += String(cString: sqlite3_errstr(result))
259-
throw StringError(error)
260286
}
261287
}
288+
289+
public enum Errors: Error {
290+
case databaseFull
291+
}
262292
}
263293

264294
private func sqlite_callback(

Tests/TSCUtilityTests/SQLiteTests.swift

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,5 +79,32 @@ class SQLiteTests: XCTestCase {
7979
configuration.busyTimeoutMilliseconds = timeout
8080
XCTAssertEqual(configuration.busyTimeoutMilliseconds, timeout)
8181
XCTAssertEqual(configuration._busyTimeoutSeconds, Int32(Double(timeout) / 1000))
82+
83+
let maxSizeInBytes = Int.random(in: 1000 ... 10000)
84+
configuration.maxSizeInBytes = maxSizeInBytes
85+
XCTAssertEqual(configuration.maxSizeInBytes, maxSizeInBytes)
86+
XCTAssertEqual(configuration.maxSizeInMegabytes, maxSizeInBytes / (1024 * 1024))
87+
}
88+
89+
func testMaxSize() throws {
90+
var configuration = SQLite.Configuration()
91+
configuration.maxSizeInBytes = 1024
92+
let db = try SQLite(location: .memory, configuration: configuration)
93+
defer { XCTAssertNoThrow(try db.close()) }
94+
95+
func generateData() throws {
96+
let tableName = UUID().uuidString
97+
try db.exec(query: "CREATE TABLE \"\(tableName)\" (ID INT PRIMARY KEY, NAME STRING);")
98+
for index in 0 ..< 1024 {
99+
let statement = try db.prepare(query: "INSERT INTO \"\(tableName)\" VALUES (?, ?);")
100+
defer { XCTAssertNoThrow(try statement.finalize()) }
101+
try statement.bind([.int(index), .string(UUID().uuidString)])
102+
try statement.step()
103+
}
104+
}
105+
106+
XCTAssertThrowsError(try generateData(), "expected error", { error in
107+
XCTAssertEqual(error as? SQLite.Errors, .databaseFull, "Expected 'database or disk is full' error")
108+
})
82109
}
83110
}

0 commit comments

Comments
 (0)