Skip to content

Commit a2d779a

Browse files
authored
updates to SQLite (#156)
motivation: expand the API of SQLite to allow more sophisticated use cases changes: * provide initiallizers for in-memory and temp file modes * move timeout to configuration for future extensibility * make Row value accesssors public * add tests * refactor testing util mktmpdir to throw instead of use XCFail which more correctly report errors, and adjust call sites
1 parent c52a61c commit a2d779a

File tree

12 files changed

+259
-154
lines changed

12 files changed

+259
-154
lines changed

Sources/TSCTestSupport/misc.swift

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -25,27 +25,21 @@ public enum Configuration {
2525
}
2626

2727
/// Test helper utility for executing a block with a temporary directory.
28-
public func mktmpdir(
28+
public func testWithTemporaryDirectory(
2929
function: StaticString = #function,
30-
file: StaticString = #file,
31-
line: UInt = #line,
3230
body: (AbsolutePath) throws -> Void
33-
) {
34-
do {
35-
let cleanedFunction = function.description
36-
.replacingOccurrences(of: "(", with: "")
37-
.replacingOccurrences(of: ")", with: "")
38-
.replacingOccurrences(of: ".", with: "")
39-
try withTemporaryDirectory(prefix: "spm-tests-\(cleanedFunction)") { tmpDirPath in
40-
defer {
41-
// Unblock and remove the tmp dir on deinit.
42-
try? localFileSystem.chmod(.userWritable, path: tmpDirPath, options: [.recursive])
43-
try? localFileSystem.removeFileTree(tmpDirPath)
44-
}
45-
try body(tmpDirPath)
31+
) throws {
32+
let cleanedFunction = function.description
33+
.replacingOccurrences(of: "(", with: "")
34+
.replacingOccurrences(of: ")", with: "")
35+
.replacingOccurrences(of: ".", with: "")
36+
try withTemporaryDirectory(prefix: "spm-tests-\(cleanedFunction)") { tmpDirPath in
37+
defer {
38+
// Unblock and remove the tmp dir on deinit.
39+
try? localFileSystem.chmod(.userWritable, path: tmpDirPath, options: [.recursive])
40+
try? localFileSystem.removeFileTree(tmpDirPath)
4641
}
47-
} catch {
48-
XCTFail("\(error)", file: file, line: line)
42+
try body(tmpDirPath)
4943
}
5044
}
5145

Sources/TSCUtility/PersistenceCache.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public final class SQLiteBackedPersistentCache: PersistentCacheProtocol {
4141
}
4242

4343
public convenience init(cacheFilePath: AbsolutePath) throws {
44-
let db = try SQLite(dbPath: cacheFilePath)
44+
let db = try SQLite(location: .path(cacheFilePath))
4545
try self.init(db: db)
4646
}
4747

Sources/TSCUtility/SQLite.swift

Lines changed: 133 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
See http://swift.org/LICENSE.txt for license information
88
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
9-
*/
9+
*/
1010

1111
import Foundation
1212
import TSCBasic
@@ -15,6 +15,104 @@ import TSCBasic
1515

1616
/// A minimal SQLite wrapper.
1717
public struct SQLite {
18+
/// The location of the database.
19+
public let location: Location
20+
21+
/// The configuration for the database.
22+
public let configuration: Configuration
23+
24+
/// Pointer to the database.
25+
let db: OpaquePointer
26+
27+
/// Create or open the database at the given path.
28+
///
29+
/// The database is opened in serialized mode.
30+
public init(location: Location, configuration: Configuration = Configuration()) throws {
31+
self.location = location
32+
self.configuration = configuration
33+
34+
var handle: OpaquePointer?
35+
try Self.checkError("Unable to open database at \(self.location)") {
36+
sqlite3_open_v2(
37+
location.pathString,
38+
&handle,
39+
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX,
40+
nil
41+
)
42+
}
43+
44+
guard let db = handle else {
45+
throw StringError("Unable to open database at \(self.location)")
46+
}
47+
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.busyTimeoutSeconds) }
50+
}
51+
52+
@available(*, deprecated, message: "use init(location:configuration) instead")
53+
public init(dbPath: AbsolutePath) throws {
54+
try self.init(location: .path(dbPath))
55+
}
56+
57+
/// Prepare the given query.
58+
public func prepare(query: String) throws -> PreparedStatement {
59+
try PreparedStatement(db: self.db, query: query)
60+
}
61+
62+
/// Directly execute the given query.
63+
///
64+
/// Note: Use withCString for string arguments.
65+
public func exec(query queryString: String, args: [CVarArg] = [], _ callback: SQLiteExecCallback? = nil) throws {
66+
let query = withVaList(args) { ptr in
67+
sqlite3_vmprintf(queryString, ptr)
68+
}
69+
70+
let wcb = callback.map { CallbackWrapper($0) }
71+
let callbackCtx = wcb.map { Unmanaged.passUnretained($0).toOpaque() }
72+
73+
var err: UnsafeMutablePointer<Int8>?
74+
try Self.checkError { sqlite3_exec(db, query, sqlite_callback, callbackCtx, &err) }
75+
76+
sqlite3_free(query)
77+
78+
if let err = err {
79+
let errorString = String(cString: err)
80+
sqlite3_free(err)
81+
throw StringError(errorString)
82+
}
83+
}
84+
85+
public func close() throws {
86+
try Self.checkError { sqlite3_close(db) }
87+
}
88+
89+
public typealias SQLiteExecCallback = ([Column]) -> Void
90+
91+
public struct Configuration {
92+
public var busyTimeoutSeconds: Int32
93+
94+
public init() {
95+
self.busyTimeoutSeconds = 5
96+
}
97+
}
98+
99+
public enum Location {
100+
case path(AbsolutePath)
101+
case memory
102+
case temporary
103+
104+
var pathString: String {
105+
switch self {
106+
case .path(let path):
107+
return path.pathString
108+
case .memory:
109+
return ":memory:"
110+
case .temporary:
111+
return ""
112+
}
113+
}
114+
}
115+
18116
/// Represents an sqlite value.
19117
public enum SQLiteValue {
20118
case null
@@ -29,23 +127,28 @@ public struct SQLite {
29127
let stmt: OpaquePointer
30128

31129
/// Get integer at the given column index.
32-
func int(at index: Int32) -> Int {
33-
Int(sqlite3_column_int64(stmt, index))
130+
public func int(at index: Int32) -> Int {
131+
Int(sqlite3_column_int64(self.stmt, index))
34132
}
35133

36134
/// Get blob data at the given column index.
37-
func blob(at index: Int32) -> Data {
135+
public func blob(at index: Int32) -> Data {
38136
let bytes = sqlite3_column_blob(stmt, index)!
39137
let count = sqlite3_column_bytes(stmt, index)
40138
return Data(bytes: bytes, count: Int(count))
41139
}
42140

43141
/// Get string at the given column index.
44-
func string(at index: Int32) -> String {
45-
return String(cString: sqlite3_column_text(stmt, index))
142+
public func string(at index: Int32) -> String {
143+
return String(cString: sqlite3_column_text(self.stmt, index))
46144
}
47145
}
48146

147+
public struct Column {
148+
public var name: String
149+
public var value: String
150+
}
151+
49152
/// Represents a prepared statement.
50153
public struct PreparedStatement {
51154
typealias sqlite3_destructor_type = (@convention(c) (UnsafeMutableRawPointer?) -> Void)
@@ -57,7 +160,7 @@ public struct SQLite {
57160

58161
public init(db: OpaquePointer, query: String) throws {
59162
var stmt: OpaquePointer?
60-
try sqlite { sqlite3_prepare_v2(db, query, -1, &stmt, nil) }
163+
try checkError { sqlite3_prepare_v2(db, query, -1, &stmt, nil) }
61164
self.stmt = stmt!
62165
}
63166

@@ -70,7 +173,7 @@ public struct SQLite {
70173
case SQLITE_DONE:
71174
return nil
72175
case SQLITE_ROW:
73-
return Row(stmt: stmt)
176+
return Row(stmt: self.stmt)
74177
default:
75178
throw StringError(String(cString: sqlite3_errstr(result)))
76179
}
@@ -82,13 +185,13 @@ public struct SQLite {
82185
let idx = Int32(idx) + 1
83186
switch argument {
84187
case .null:
85-
try sqlite { sqlite3_bind_null(stmt, idx) }
188+
try checkError { sqlite3_bind_null(stmt, idx) }
86189
case .int(let int):
87-
try sqlite { sqlite3_bind_int64(stmt, idx, Int64(int)) }
190+
try checkError { sqlite3_bind_int64(stmt, idx, Int64(int)) }
88191
case .string(let str):
89-
try sqlite { sqlite3_bind_text(stmt, idx, str, -1, Self.SQLITE_TRANSIENT) }
192+
try checkError { sqlite3_bind_text(stmt, idx, str, -1, Self.SQLITE_TRANSIENT) }
90193
case .blob(let blob):
91-
try sqlite {
194+
try checkError {
92195
blob.withUnsafeBytes { ptr in
93196
sqlite3_bind_blob(
94197
stmt,
@@ -105,91 +208,37 @@ public struct SQLite {
105208

106209
/// Reset the prepared statement.
107210
public func reset() throws {
108-
try sqlite { sqlite3_reset(stmt) }
211+
try checkError { sqlite3_reset(stmt) }
109212
}
110213

111214
/// Clear bindings from the prepared statment.
112215
public func clearBindings() throws {
113-
try sqlite { sqlite3_clear_bindings(stmt) }
216+
try checkError { sqlite3_clear_bindings(stmt) }
114217
}
115218

116219
/// Finalize the statement and free up resources.
117220
public func finalize() throws {
118-
try sqlite { sqlite3_finalize(stmt) }
221+
try checkError { sqlite3_finalize(stmt) }
119222
}
120223
}
121224

122-
/// The path to the database file.
123-
public let dbPath: AbsolutePath
124-
125-
/// Pointer to the database.
126-
let db: OpaquePointer
127-
128-
/// Create or open the database at the given path.
129-
///
130-
/// The database is opened in serialized mode.
131-
public init(dbPath: AbsolutePath) throws {
132-
self.dbPath = dbPath
133-
134-
var db: OpaquePointer? = nil
135-
try sqlite("unable to open database at \(dbPath)") {
136-
sqlite3_open_v2(
137-
dbPath.pathString,
138-
&db,
139-
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX,
140-
nil
141-
)
225+
fileprivate class CallbackWrapper {
226+
var callback: SQLiteExecCallback
227+
init(_ callback: @escaping SQLiteExecCallback) {
228+
self.callback = callback
142229
}
143-
144-
self.db = db!
145-
try sqlite { sqlite3_extended_result_codes(db, 1) }
146-
try sqlite { sqlite3_busy_timeout(db, 5 * 1000 /* 5s */) }
147-
}
148-
149-
/// Prepare the given query.
150-
public func prepare(query: String) throws -> PreparedStatement {
151-
try PreparedStatement(db: db, query: query)
152230
}
153231

154-
/// Directly execute the given query.
155-
///
156-
/// Note: Use withCString for string arguments.
157-
public func exec(query: String, args: [CVarArg] = [], _ callback: SQLiteExecCallback? = nil) throws {
158-
let query = withVaList(args) { ptr in
159-
sqlite3_vmprintf(query, ptr)
160-
}
161-
162-
let wcb = callback.map { CallbackWrapper($0) }
163-
let callbackCtx = wcb.map { Unmanaged.passUnretained($0).toOpaque() }
164-
165-
var err: UnsafeMutablePointer<Int8>? = nil
166-
try sqlite { sqlite3_exec(db, query, sqlite_callback, callbackCtx, &err) }
167-
168-
if let err = err {
169-
let errorString = String(cString: err)
170-
sqlite3_free(err)
171-
throw StringError(errorString)
232+
private static func checkError(_ errorPrefix: String? = nil, _ fn: () -> Int32) throws {
233+
let result = fn()
234+
if result != SQLITE_OK {
235+
var error = ""
236+
if let errorPrefix = errorPrefix {
237+
error += errorPrefix + ": "
238+
}
239+
error += String(cString: sqlite3_errstr(result))
240+
throw StringError(error)
172241
}
173-
174-
sqlite3_free(query)
175-
}
176-
177-
public func close() throws {
178-
try sqlite { sqlite3_close(db) }
179-
}
180-
181-
public struct Column {
182-
public var name: String
183-
public var value: String
184-
}
185-
186-
public typealias SQLiteExecCallback = ([Column]) -> Void
187-
}
188-
189-
private class CallbackWrapper {
190-
var callback: SQLite.SQLiteExecCallback
191-
init(_ callback: @escaping SQLite.SQLiteExecCallback) {
192-
self.callback = callback
193242
}
194243
}
195244

@@ -204,7 +253,7 @@ private func sqlite_callback(
204253
let numColumns = Int(numColumns)
205254
var result: [SQLite.Column] = []
206255

207-
for idx in 0..<numColumns {
256+
for idx in 0 ..< numColumns {
208257
var name = ""
209258
if let ptr = columnNames.advanced(by: idx).pointee {
210259
name = String(cString: ptr)
@@ -216,20 +265,8 @@ private func sqlite_callback(
216265
result.append(SQLite.Column(name: name, value: value))
217266
}
218267

219-
let wcb = Unmanaged<CallbackWrapper>.fromOpaque(ctx).takeUnretainedValue()
268+
let wcb = Unmanaged<SQLite.CallbackWrapper>.fromOpaque(ctx).takeUnretainedValue()
220269
wcb.callback(result)
221270

222271
return 0
223272
}
224-
225-
private func sqlite(_ errorPrefix: String? = nil, _ fn: () -> Int32) throws {
226-
let result = fn()
227-
if result != SQLITE_OK {
228-
var error = ""
229-
if let errorPrefix = errorPrefix {
230-
error += errorPrefix + ": "
231-
}
232-
error += String(cString: sqlite3_errstr(result))
233-
throw StringError(error)
234-
}
235-
}

0 commit comments

Comments
 (0)