Skip to content

Commit 25da32d

Browse files
committed
Serialize SQLite access using serial queue
1 parent 1f172a5 commit 25da32d

File tree

6 files changed

+209
-175
lines changed

6 files changed

+209
-175
lines changed

.swiftformat

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
--swiftversion 5.7
2+
--binarygrouping none
3+
--decimalgrouping none
4+
--hexgrouping none
5+
--indent 2
6+
--octalgrouping none
7+
--semicolons never
8+
--wraparguments before-first
9+
--wrapcollections before-first
10+
--wrapparameters before-first
11+
--extensionacl on-declarations
12+
--maxwidth 100

Makefile

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,4 @@ test:
88

99
.PHONY: format
1010
format:
11-
@swift format \
12-
--ignore-unparsable-files \
13-
--in-place \
14-
--recursive \
15-
./Sources \
16-
./Tests \
17-
./Package.swift
11+
@swiftformat .

Package.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ let package = Package(
1313
.library(
1414
name: "Sqlite",
1515
targets: ["Sqlite"]
16-
)
16+
),
1717
],
1818
dependencies: [],
1919
targets: [
2020
.target(
2121
name: "Sqlite",
2222
dependencies: [
23-
.target(name: "Csqlite3")
23+
.target(name: "Csqlite3"),
2424
]
2525
),
2626
.testTarget(

Sources/Sqlite/SQLite.swift

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import Foundation
2+
3+
#if os(Linux)
4+
import Csqlite3
5+
#else
6+
import SQLite3
7+
#endif
8+
9+
private let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self)
10+
11+
public final class SQLite {
12+
private let queue = DispatchQueue(label: "co.binaryscraping.sqlite")
13+
public private(set) var handle: OpaquePointer?
14+
15+
/// Initialize an ``SQLite`` connection to a database at specified `path`.
16+
/// - Parameter path: path to the `.sqlite` database file.
17+
public init(path: String) throws {
18+
try validate(
19+
sqlite3_open_v2(
20+
path,
21+
&handle,
22+
SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE,
23+
nil
24+
)
25+
)
26+
}
27+
28+
/// Initialize a in memory ``SQLite`` connection.
29+
public convenience init() throws {
30+
try self.init(path: "")
31+
}
32+
33+
deinit {
34+
sqlite3_close_v2(self.handle)
35+
}
36+
37+
public func execute(_ sql: String) throws {
38+
try queue.sync {
39+
_ = try self.validate(
40+
sqlite3_exec(self.handle, sql, nil, nil, nil)
41+
)
42+
}
43+
}
44+
45+
@discardableResult
46+
public func execute(_ sql: String, _ bindings: [Datatype]) throws -> [Row] {
47+
try queue.sync {
48+
var stmt: OpaquePointer?
49+
try self.validate(sqlite3_prepare_v2(self.handle, sql, -1, &stmt, nil))
50+
defer { sqlite3_finalize(stmt) }
51+
for (idx, binding) in zip(Int32(1)..., bindings) {
52+
switch binding {
53+
case .null:
54+
try self.validate(sqlite3_bind_null(stmt, idx))
55+
case let .integer(value):
56+
try self.validate(sqlite3_bind_int64(stmt, idx, value))
57+
case let .real(value):
58+
try self.validate(sqlite3_bind_double(stmt, idx, value))
59+
case let .text(value):
60+
try self.validate(sqlite3_bind_text(stmt, idx, value, -1, SQLITE_TRANSIENT))
61+
case let .blob(value):
62+
try value.withUnsafeBytes {
63+
_ = try self.validate(
64+
sqlite3_bind_blob(stmt, idx, $0.baseAddress, Int32($0.count), SQLITE_TRANSIENT)
65+
)
66+
}
67+
}
68+
}
69+
let cols = sqlite3_column_count(stmt)
70+
var rows: [[Datatype]] = []
71+
while try self.validate(sqlite3_step(stmt)) == SQLITE_ROW {
72+
rows.append(
73+
try (0 ..< cols).map { idx -> Datatype in
74+
switch sqlite3_column_type(stmt, idx) {
75+
case SQLITE_BLOB:
76+
if let bytes = sqlite3_column_blob(stmt, idx) {
77+
let count = Int(sqlite3_column_bytes(stmt, idx))
78+
return .blob(Data(bytes: bytes, count: count))
79+
}
80+
return .blob(Data())
81+
case SQLITE_FLOAT:
82+
return .real(sqlite3_column_double(stmt, idx))
83+
case SQLITE_INTEGER:
84+
return .integer(sqlite3_column_int64(stmt, idx))
85+
case SQLITE_NULL:
86+
return .null
87+
case SQLITE_TEXT:
88+
return .text(String(cString: sqlite3_column_text(stmt, idx)))
89+
default:
90+
throw Error(description: "fatal")
91+
}
92+
}
93+
)
94+
}
95+
return rows
96+
}
97+
}
98+
99+
@discardableResult
100+
public func execute(_ sql: String, _ bindings: Datatype...) throws -> [Row] {
101+
try execute(sql, bindings)
102+
}
103+
104+
public var lastInsertRowId: Int64 {
105+
queue.sync {
106+
sqlite3_last_insert_rowid(self.handle)
107+
}
108+
}
109+
110+
@discardableResult
111+
private func validate(_ code: Int32) throws -> Int32 {
112+
guard code == SQLITE_OK || code == SQLITE_ROW || code == SQLITE_DONE
113+
else { throw Error(code: code) }
114+
return code
115+
}
116+
117+
public enum Datatype: Equatable {
118+
case blob(Data)
119+
case integer(Int64)
120+
case null
121+
case real(Double)
122+
case text(String)
123+
}
124+
125+
public typealias Row = [Datatype]
126+
127+
public struct Error: Swift.Error, Equatable {
128+
public var code: Int32?
129+
public var description: String
130+
}
131+
}
132+
133+
extension SQLite.Error {
134+
init(code: Int32) {
135+
self.code = code
136+
description = String(cString: sqlite3_errstr(code))
137+
}
138+
}
139+
140+
extension SQLite.Datatype {
141+
public var blobValue: Data? {
142+
guard case let .blob(value) = self else {
143+
return nil
144+
}
145+
146+
return value
147+
}
148+
149+
public var integerValue: Int64? {
150+
guard case let .integer(value) = self else {
151+
return nil
152+
}
153+
154+
return value
155+
}
156+
157+
public var realValue: Double? {
158+
guard case let .real(value) = self else {
159+
return nil
160+
}
161+
162+
return value
163+
}
164+
165+
public var textValue: String? {
166+
guard case let .text(value) = self else {
167+
return nil
168+
}
169+
170+
return value
171+
}
172+
173+
public var isNull: Bool { self == .null }
174+
}

Sources/Sqlite/Sqlite.swift

Lines changed: 0 additions & 158 deletions
This file was deleted.

0 commit comments

Comments
 (0)