Skip to content

Commit 239f317

Browse files
authored
Basics: migrate TSCUtility.SQLite to Basics (#5764)
This moves the SQLite API wrapper to SPM from tools-support-core. There is currently a single user of this API, which is SPM. By migrating the API to SPM we can deprecate the TSC interfaces to prevent new adoption. It also reduces the dependencies on `TSCUtility` for SPM which works towards the longer term goal of shedding the tools-support-core dependency.
1 parent d1d2d17 commit 239f317

File tree

12 files changed

+351
-8
lines changed

12 files changed

+351
-8
lines changed

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ endif()
5959

6060
find_package(dispatch QUIET)
6161
find_package(Foundation QUIET)
62+
find_package(SQLite3 REQUIRED)
6263

6364
add_subdirectory(Sources)
6465
add_subdirectory(cmake/modules)

Package.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,9 +170,12 @@ let package = Package(
170170

171171
// MARK: SwiftPM specific support libraries
172172

173+
.systemLibrary(name: "SPMSQLite3", pkgConfig: "sqlite3"),
174+
173175
.target(
174176
name: "Basics",
175177
dependencies: [
178+
"SPMSQLite3",
176179
.product(name: "OrderedCollections", package: "swift-collections"),
177180
.product(name: "SwiftToolsSupport-auto", package: "swift-tools-support-core"),
178181
.product(name: "SystemPackage", package: "swift-system"),

Sources/Basics/CMakeLists.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ add_library(Basics
2525
Netrc.swift
2626
NSLock+Extensions.swift
2727
Observability.swift
28+
SQLite.swift
2829
Sandbox.swift
2930
String+Extensions.swift
3031
Triple+Extensions.swift
@@ -38,7 +39,8 @@ target_link_libraries(Basics PUBLIC
3839
TSCBasic
3940
TSCUtility)
4041
target_link_libraries(Basics PRIVATE
41-
TSCclibc)
42+
SPMSQLite3
43+
TSCclibc)
4244
# NOTE(compnerd) workaround for CMake not setting up include flags yet
4345
set_target_properties(Basics PROPERTIES
4446
INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY})

Sources/Basics/SQLite.swift

Lines changed: 322 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See http://swift.org/LICENSE.txt for license information
8+
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
import Foundation
12+
13+
import TSCBasic
14+
15+
@_implementationOnly import SPMSQLite3
16+
17+
/// A minimal SQLite wrapper.
18+
public struct SQLite {
19+
/// The location of the database.
20+
public let location: Location
21+
22+
/// The configuration for the database.
23+
public let configuration: Configuration
24+
25+
/// Pointer to the database.
26+
let db: OpaquePointer
27+
28+
/// Create or open the database at the given path.
29+
///
30+
/// The database is opened in serialized mode.
31+
public init(location: Location, configuration: Configuration = Configuration()) throws {
32+
self.location = location
33+
self.configuration = configuration
34+
35+
var handle: OpaquePointer?
36+
try Self.checkError ({
37+
sqlite3_open_v2(
38+
location.pathString,
39+
&handle,
40+
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX,
41+
nil
42+
)
43+
},
44+
description: "Unable to open database at \(self.location)")
45+
46+
guard let db = handle else {
47+
throw StringError("Unable to open database at \(self.location)")
48+
}
49+
self.db = db
50+
try Self.checkError({ sqlite3_extended_result_codes(db, 1) }, description: "Unable to configure database")
51+
try Self.checkError({ sqlite3_busy_timeout(db, self.configuration.busyTimeoutMilliseconds) }, description: "Unable to configure database busy timeout")
52+
if let maxPageCount = self.configuration.maxPageCount {
53+
try self.exec(query: "PRAGMA max_page_count=\(maxPageCount);")
54+
}
55+
}
56+
57+
@available(*, deprecated, message: "use init(location:configuration) instead")
58+
public init(dbPath: AbsolutePath) throws {
59+
try self.init(location: .path(dbPath))
60+
}
61+
62+
/// Prepare the given query.
63+
public func prepare(query: String) throws -> PreparedStatement {
64+
try PreparedStatement(db: self.db, query: query)
65+
}
66+
67+
/// Directly execute the given query.
68+
///
69+
/// Note: Use withCString for string arguments.
70+
public func exec(query queryString: String, args: [CVarArg] = [], _ callback: SQLiteExecCallback? = nil) throws {
71+
let query = withVaList(args) { ptr in
72+
sqlite3_vmprintf(queryString, ptr)
73+
}
74+
75+
let wcb = callback.map { CallbackWrapper($0) }
76+
let callbackCtx = wcb.map { Unmanaged.passUnretained($0).toOpaque() }
77+
78+
var err: UnsafeMutablePointer<Int8>?
79+
try Self.checkError { sqlite3_exec(db, query, sqlite_callback, callbackCtx, &err) }
80+
81+
sqlite3_free(query)
82+
83+
if let err = err {
84+
let errorString = String(cString: err)
85+
sqlite3_free(err)
86+
throw StringError(errorString)
87+
}
88+
}
89+
90+
public func close() throws {
91+
try Self.checkError { sqlite3_close(db) }
92+
}
93+
94+
public typealias SQLiteExecCallback = ([Column]) -> Void
95+
96+
public struct Configuration {
97+
public var busyTimeoutMilliseconds: Int32
98+
public var maxSizeInBytes: Int?
99+
100+
// https://www.sqlite.org/pgszchng2016.html
101+
private let defaultPageSizeInBytes = 1024
102+
103+
public init() {
104+
self.busyTimeoutMilliseconds = 5000
105+
self.maxSizeInBytes = .none
106+
}
107+
108+
// FIXME: deprecated 12/2020, remove once clients migrated over
109+
@available(*, deprecated, message: "use busyTimeout instead")
110+
public var busyTimeoutSeconds: Int32 {
111+
get {
112+
self._busyTimeoutSeconds
113+
} set {
114+
self._busyTimeoutSeconds = newValue
115+
}
116+
}
117+
118+
// so tests dont warn
119+
internal var _busyTimeoutSeconds: Int32 {
120+
get {
121+
return Int32(truncatingIfNeeded: Int(Double(self.busyTimeoutMilliseconds) / 1000))
122+
} set {
123+
self.busyTimeoutMilliseconds = newValue * 1000
124+
}
125+
}
126+
127+
public var maxSizeInMegabytes: Int? {
128+
get {
129+
self.maxSizeInBytes.map { $0 / (1024 * 1024) }
130+
}
131+
set {
132+
self.maxSizeInBytes = newValue.map { $0 * 1024 * 1024 }
133+
}
134+
}
135+
136+
public var maxPageCount: Int? {
137+
self.maxSizeInBytes.map { $0 / self.defaultPageSizeInBytes }
138+
}
139+
}
140+
141+
public enum Location {
142+
case path(AbsolutePath)
143+
case memory
144+
case temporary
145+
146+
var pathString: String {
147+
switch self {
148+
case .path(let path):
149+
return path.pathString
150+
case .memory:
151+
return ":memory:"
152+
case .temporary:
153+
return ""
154+
}
155+
}
156+
}
157+
158+
/// Represents an sqlite value.
159+
public enum SQLiteValue {
160+
case null
161+
case string(String)
162+
case int(Int)
163+
case blob(Data)
164+
}
165+
166+
/// Represents a row returned by called step() on a prepared statement.
167+
public struct Row {
168+
/// The pointer to the prepared statment.
169+
let stmt: OpaquePointer
170+
171+
/// Get integer at the given column index.
172+
public func int(at index: Int32) -> Int {
173+
Int(sqlite3_column_int64(self.stmt, index))
174+
}
175+
176+
/// Get blob data at the given column index.
177+
public func blob(at index: Int32) -> Data {
178+
let bytes = sqlite3_column_blob(stmt, index)!
179+
let count = sqlite3_column_bytes(stmt, index)
180+
return Data(bytes: bytes, count: Int(count))
181+
}
182+
183+
/// Get string at the given column index.
184+
public func string(at index: Int32) -> String {
185+
return String(cString: sqlite3_column_text(self.stmt, index))
186+
}
187+
}
188+
189+
public struct Column {
190+
public var name: String
191+
public var value: String
192+
}
193+
194+
/// Represents a prepared statement.
195+
public struct PreparedStatement {
196+
typealias sqlite3_destructor_type = (@convention(c) (UnsafeMutableRawPointer?) -> Void)
197+
static let SQLITE_STATIC = unsafeBitCast(0, to: sqlite3_destructor_type.self)
198+
static let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self)
199+
200+
/// The pointer to the prepared statment.
201+
let stmt: OpaquePointer
202+
203+
public init(db: OpaquePointer, query: String) throws {
204+
var stmt: OpaquePointer?
205+
try SQLite.checkError { sqlite3_prepare_v2(db, query, -1, &stmt, nil) }
206+
self.stmt = stmt!
207+
}
208+
209+
/// Evaluate the prepared statement.
210+
@discardableResult
211+
public func step() throws -> Row? {
212+
let result = sqlite3_step(stmt)
213+
214+
switch result {
215+
case SQLITE_DONE:
216+
return nil
217+
case SQLITE_ROW:
218+
return Row(stmt: self.stmt)
219+
default:
220+
throw StringError(String(cString: sqlite3_errstr(result)))
221+
}
222+
}
223+
224+
/// Bind the given arguments to the statement.
225+
public func bind(_ arguments: [SQLiteValue]) throws {
226+
for (idx, argument) in arguments.enumerated() {
227+
let idx = Int32(idx) + 1
228+
switch argument {
229+
case .null:
230+
try checkError { sqlite3_bind_null(stmt, idx) }
231+
case .int(let int):
232+
try checkError { sqlite3_bind_int64(stmt, idx, Int64(int)) }
233+
case .string(let str):
234+
try checkError { sqlite3_bind_text(stmt, idx, str, -1, Self.SQLITE_TRANSIENT) }
235+
case .blob(let blob):
236+
try checkError {
237+
blob.withUnsafeBytes { ptr in
238+
sqlite3_bind_blob(
239+
stmt,
240+
idx,
241+
ptr.baseAddress,
242+
Int32(blob.count),
243+
Self.SQLITE_TRANSIENT
244+
)
245+
}
246+
}
247+
}
248+
}
249+
}
250+
251+
/// Reset the prepared statement.
252+
public func reset() throws {
253+
try SQLite.checkError { sqlite3_reset(stmt) }
254+
}
255+
256+
/// Clear bindings from the prepared statment.
257+
public func clearBindings() throws {
258+
try SQLite.checkError { sqlite3_clear_bindings(stmt) }
259+
}
260+
261+
/// Finalize the statement and free up resources.
262+
public func finalize() throws {
263+
try SQLite.checkError { sqlite3_finalize(stmt) }
264+
}
265+
}
266+
267+
fileprivate class CallbackWrapper {
268+
var callback: SQLiteExecCallback
269+
init(_ callback: @escaping SQLiteExecCallback) {
270+
self.callback = callback
271+
}
272+
}
273+
274+
private static func checkError(_ fn: () -> Int32, description prefix: String? = .none) throws {
275+
let result = fn()
276+
if result != SQLITE_OK {
277+
var description = String(cString: sqlite3_errstr(result))
278+
switch description.lowercased() {
279+
case "database or disk is full":
280+
throw Errors.databaseFull
281+
default:
282+
if let prefix = prefix {
283+
description = "\(prefix): \(description)"
284+
}
285+
throw StringError(description)
286+
}
287+
}
288+
}
289+
290+
public enum Errors: Error {
291+
case databaseFull
292+
}
293+
}
294+
295+
private func sqlite_callback(
296+
_ ctx: UnsafeMutableRawPointer?,
297+
_ numColumns: Int32,
298+
_ columns: UnsafeMutablePointer<UnsafeMutablePointer<Int8>?>?,
299+
_ columnNames: UnsafeMutablePointer<UnsafeMutablePointer<Int8>?>?
300+
) -> Int32 {
301+
guard let ctx = ctx else { return 0 }
302+
guard let columnNames = columnNames, let columns = columns else { return 0 }
303+
let numColumns = Int(numColumns)
304+
var result: [SQLite.Column] = []
305+
306+
for idx in 0 ..< numColumns {
307+
var name = ""
308+
if let ptr = columnNames.advanced(by: idx).pointee {
309+
name = String(cString: ptr)
310+
}
311+
var value = ""
312+
if let ptr = columns.advanced(by: idx).pointee {
313+
value = String(cString: ptr)
314+
}
315+
result.append(SQLite.Column(name: name, value: value))
316+
}
317+
318+
let wcb = Unmanaged<SQLite.CallbackWrapper>.fromOpaque(ctx).takeUnretainedValue()
319+
wcb.callback(result)
320+
321+
return 0
322+
}

Sources/Basics/SQLiteBackedCache.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
import Foundation
1414

1515
import TSCBasic
16-
import struct TSCUtility.SQLite
1716

1817
/// SQLite backed persistent cache.
1918
public final class SQLiteBackedCache<Value: Codable>: Closable {

Sources/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
# See http://swift.org/LICENSE.txt for license information
77
# See http://swift.org/CONTRIBUTORS.txt for Swift project authors
88

9+
add_subdirectory(SPMSQLite3)
910
add_subdirectory(Basics)
1011
add_subdirectory(Build)
1112
add_subdirectory(Commands)

Sources/PackageCollections/Storage/SQLitePackageCollectionsStorage.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ import struct Foundation.URL
2020
import PackageModel
2121
import TSCBasic
2222

23-
import struct TSCUtility.SQLite
24-
2523
final class SQLitePackageCollectionsStorage: PackageCollectionsStorage, Closable {
2624
private static let packageCollectionsTableName = "package_collections"
2725
private static let packagesFTSName = "fts_packages"

0 commit comments

Comments
 (0)