Skip to content

Commit 426d1cc

Browse files
authored
Merge pull request #9 from PureSwift/feature/dependencies
Migrate from SQLite.swift to PureSwift/SQLite
2 parents 27f305f + 6fa685a commit 426d1cc

12 files changed

Lines changed: 147 additions & 230 deletions

File tree

‎.github/workflows/swift.yml‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,21 @@ jobs:
3838
- name: Test
3939
run: swift test -c ${{ matrix.config }}
4040

41+
windows:
42+
runs-on: windows-latest
43+
steps:
44+
- uses: compnerd/gha-setup-swift@main
45+
with:
46+
branch: swift-6.3.2-release
47+
tag: 6.3.2-RELEASE
48+
- uses: actions/checkout@v4
49+
- name: Swift version
50+
run: swift --version
51+
- name: Build
52+
run: swift build -v
53+
- name: Test
54+
run: swift test -v
55+
4156
skip-fuse:
4257
name: Skip Fuse (Android)
4358
runs-on: macos-26

‎Package.swift‎

Lines changed: 6 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,6 @@
22
import PackageDescription
33
import Foundation
44

5-
// Android has no system libsqlite3, so it needs SQLite.swift's embedded
6-
// copy. macOS and Linux keep SQLite.swift's own default for the platform.
7-
let isAndroid = ProcessInfo.processInfo.environment["TARGET_OS_ANDROID"] == "1"
8-
9-
let sqliteDependency: Package.Dependency = isAndroid
10-
? .package(
11-
url: "https://github.com/stephencelis/SQLite.swift.git",
12-
from: "0.16.0",
13-
traits: ["SQLiteSwiftCSQLite"]
14-
)
15-
: .package(
16-
url: "https://github.com/stephencelis/SQLite.swift.git",
17-
from: "0.16.0"
18-
)
19-
205
let package = Package(
216
name: "CoreModel-SQLite",
227
platforms: [
@@ -36,13 +21,12 @@ let package = Package(
3621
url: "https://github.com/PureSwift/CoreModel",
3722
from: "2.8.0"
3823
),
39-
sqliteDependency,
40-
// Off Apple platforms, SQLite.swift links the embedded SQLite from this package.
41-
// We depend on it directly so we can call the SQLite C API (custom functions)
42-
// where the system `SQLite3` module isn't available.
24+
// PureSwift/SQLite wraps the system SQLite3 on Apple platforms and links
25+
// an embedded SQLite (via swift-sqlcipher) everywhere else, so no separate
26+
// C SQLite dependency or per-platform branching is needed here.
4327
.package(
44-
url: "https://github.com/stephencelis/CSQLite",
45-
from: "3.50.4"
28+
url: "https://github.com/PureSwift/SQLite",
29+
branch: "master"
4630
)
4731
],
4832
targets: [
@@ -52,14 +36,7 @@ let package = Package(
5236
"CoreModel",
5337
.product(
5438
name: "SQLite",
55-
package: "SQLite.swift"
56-
),
57-
// On Apple platforms the SQLite C API comes from the system `SQLite3`
58-
// module; elsewhere it comes from SQLite.swift's embedded copy.
59-
.product(
60-
name: "SQLiteSwiftCSQLite",
61-
package: "CSQLite",
62-
condition: .when(platforms: [.linux, .android])
39+
package: "SQLite"
6340
)
6441
]
6542
),

‎Sources/CoreModelSQLite/AttributeValue.swift‎

Lines changed: 42 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -12,34 +12,36 @@ import SQLite
1212
internal extension AttributeValue {
1313

1414
/// Convert to a SQLite binding value.
15+
///
16+
/// `nil` represents a SQL `NULL`.
1517
var binding: Binding? {
1618
switch self {
1719
case .null:
1820
return nil
1921
case let .string(value):
20-
return value
22+
return .text(value)
2123
case let .uuid(value):
22-
return value.uuidString
24+
return .text(value.uuidString)
2325
case let .url(value):
24-
return value.absoluteString
26+
return .text(value.absoluteString)
2527
case let .data(value):
26-
return Blob(bytes: [UInt8](value))
28+
return Blob(bytes: [UInt8](value)).binding
2729
case let .date(value):
28-
return value.timeIntervalSince1970
30+
return .double(value.timeIntervalSince1970)
2931
case let .bool(value):
30-
return Int64(value ? 1 : 0)
32+
return .integer(value ? 1 : 0)
3133
case let .int16(value):
32-
return Int64(value)
34+
return .integer(Int64(value))
3335
case let .int32(value):
34-
return Int64(value)
36+
return .integer(Int64(value))
3537
case let .int64(value):
36-
return value
38+
return .integer(value)
3739
case let .float(value):
38-
return Double(value)
40+
return .double(Double(value))
3941
case let .double(value):
40-
return value
42+
return .double(value)
4143
case let .decimal(value):
42-
return value.description
44+
return .text(value.description)
4345
}
4446
}
4547

@@ -81,27 +83,27 @@ internal extension AttributeValue {
8183
}
8284
self = .double(value)
8385
case .string:
84-
guard let value = binding as? String else {
86+
guard let value = binding.textValue else {
8587
throw SQLiteDatabaseError.invalidBinding(binding, type)
8688
}
8789
self = .string(value)
8890
case .data:
89-
guard let value = binding as? Blob else {
91+
guard let value = binding.blobValue else {
9092
throw SQLiteDatabaseError.invalidBinding(binding, type)
9193
}
92-
self = .data(Data(value.bytes))
94+
self = .data(Data(value))
9395
case .date:
9496
guard let value = binding.doubleValue else {
9597
throw SQLiteDatabaseError.invalidBinding(binding, type)
9698
}
9799
self = .date(Date(timeIntervalSince1970: value))
98100
case .uuid:
99-
guard let string = binding as? String, let value = UUID(uuidString: string) else {
101+
guard let string = binding.textValue, let value = UUID(uuidString: string) else {
100102
throw SQLiteDatabaseError.invalidBinding(binding, type)
101103
}
102104
self = .uuid(value)
103105
case .url:
104-
guard let string = binding as? String, let value = URL(string: string) else {
106+
guard let string = binding.textValue, let value = URL(string: string) else {
105107
throw SQLiteDatabaseError.invalidBinding(binding, type)
106108
}
107109
self = .url(value)
@@ -114,43 +116,44 @@ internal extension AttributeValue {
114116
}
115117
}
116118

117-
private extension Binding {
119+
internal extension Binding {
118120

121+
/// The integer value, converting `REAL` and `TEXT` where possible.
119122
var int64Value: Int64? {
120-
switch self {
121-
case let value as Int64:
122-
return value
123-
case let value as Double:
124-
return Int64(exactly: value)
125-
case let value as String:
126-
return Int64(value)
127-
default:
123+
integer
124+
}
125+
126+
/// The floating-point value, converting `INTEGER` and `TEXT` where possible.
127+
var doubleValue: Double? {
128+
double
129+
}
130+
131+
/// The stored text value, without converting numeric storage classes to text.
132+
var textValue: String? {
133+
guard case let .text(value) = self else {
128134
return nil
129135
}
136+
return value
130137
}
131138

132-
var doubleValue: Double? {
133-
switch self {
134-
case let value as Double:
135-
return value
136-
case let value as Int64:
137-
return Double(value)
138-
case let value as String:
139-
return Double(value)
140-
default:
139+
/// The stored blob bytes, only for `BLOB` values.
140+
var blobValue: [UInt8]? {
141+
guard case .blob = self else {
141142
return nil
142143
}
144+
return bytes
143145
}
144146

147+
/// The decimal value, parsed from `TEXT` or converted from a numeric storage class.
145148
var decimalValue: Decimal? {
146149
switch self {
147-
case let value as String:
150+
case let .text(value):
148151
return Decimal(string: value)
149-
case let value as Double:
152+
case let .double(value):
150153
return Decimal(value)
151-
case let value as Int64:
154+
case let .integer(value):
152155
return Decimal(value)
153-
default:
156+
case .blob, .null:
154157
return nil
155158
}
156159
}

‎Sources/CoreModelSQLite/CustomFunction.swift‎

Lines changed: 27 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -8,106 +8,42 @@
88
import Foundation
99
import CoreModel
1010
import SQLite
11-
#if canImport(Darwin)
12-
import SQLite3
13-
#elseif canImport(SQLiteSwiftCSQLite)
14-
import SQLiteSwiftCSQLite
15-
#elseif canImport(CSQLite)
16-
import CSQLite
17-
#else
18-
import SQLite3
19-
#endif
20-
21-
// Registers custom scalar functions by calling `sqlite3_create_function_v2` directly
22-
// with `@convention(c)` function pointers, rather than SQLite.swift's `createFunction`.
23-
// SQLite.swift registers the callback with a `@convention(block)` closure cast to a raw
24-
// pointer, which is unreliable off Apple platforms (upstream
25-
// https://github.com/stephencelis/SQLite.swift/issues/1071). A plain C function pointer
26-
// plus a retained context pointer works identically on every platform.
27-
28-
/// The SQLite `SQLITE_TRANSIENT` sentinel destructor, telling SQLite to copy a result
29-
/// value immediately (it is a macro in C, so it isn't imported).
30-
private let transientDestructor = unsafeBitCast(-1, to: sqlite3_destructor_type.self)
31-
32-
/// Retains a ``DatabaseFunction`` so it can be passed through SQLite as an opaque pointer
33-
/// and recovered inside the C callback.
34-
private final class FunctionBox {
35-
let function: DatabaseFunction
36-
init(_ function: DatabaseFunction) { self.function = function }
37-
}
3811

3912
internal extension SQLite.Connection {
4013

41-
/// Registers a `DatabaseFunction` with this connection via the SQLite C API.
14+
/// Registers a `DatabaseFunction` as a custom SQL scalar function.
4215
func register(function: DatabaseFunction) throws {
43-
let box = Unmanaged.passRetained(FunctionBox(function))
44-
let flags = SQLITE_UTF8 | (function.deterministic ? SQLITE_DETERMINISTIC : 0)
45-
let argumentCount = function.argumentCount.map { Int32($0) } ?? -1
46-
let code = sqlite3_create_function_v2(
47-
handle,
16+
let evaluate = function.evaluate
17+
try createFunction(
4818
function.name,
49-
argumentCount,
50-
flags,
51-
box.toOpaque(),
52-
{ context, argc, argv in
53-
let function = Unmanaged<FunctionBox>.fromOpaque(sqlite3_user_data(context)).takeUnretainedValue().function
54-
var arguments = [AttributeValue?]()
55-
arguments.reserveCapacity(Int(argc))
56-
for index in 0..<Int(argc) {
57-
arguments.append(argumentValue(argv?[index]))
58-
}
59-
setResult(context, function.evaluate(arguments))
60-
},
61-
nil, // xStep (scalar function, no aggregate)
62-
nil, // xFinal
63-
{ pointer in
64-
// Balance `passRetained` when SQLite drops the function.
65-
guard let pointer else { return }
66-
Unmanaged<FunctionBox>.fromOpaque(pointer).release()
19+
argumentCount: function.argumentCount.map { Int32($0) },
20+
deterministic: function.deterministic
21+
) { arguments in
22+
let values: [AttributeValue?] = arguments.map { AttributeValue(functionArgument: $0) }
23+
guard let result = evaluate(values), let binding = result.binding else {
24+
return .null
6725
}
68-
)
69-
guard code == SQLITE_OK else {
70-
box.release() // xDestroy isn't called when registration fails
71-
throw SQLiteDatabaseError.unableToCreateFunction(function.name, code)
26+
return binding
7227
}
7328
}
7429
}
7530

76-
/// Read a SQLite argument value into an ``AttributeValue``, inferring its shape from the
77-
/// value's runtime storage class.
78-
private func argumentValue(_ value: OpaquePointer?) -> AttributeValue {
79-
switch sqlite3_value_type(value) {
80-
case SQLITE_INTEGER:
81-
return .int64(sqlite3_value_int64(value))
82-
case SQLITE_FLOAT:
83-
return .double(sqlite3_value_double(value))
84-
case SQLITE_TEXT:
85-
guard let text = sqlite3_value_text(value) else { return .null }
86-
return .string(String(cString: text))
87-
case SQLITE_BLOB:
88-
guard let bytes = sqlite3_value_blob(value) else { return .data(Data()) }
89-
return .data(Data(bytes: bytes, count: Int(sqlite3_value_bytes(value))))
90-
default:
91-
return .null
92-
}
93-
}
94-
95-
/// Set a function's result on the SQLite context from an ``AttributeValue``.
96-
private func setResult(_ context: OpaquePointer?, _ value: AttributeValue?) {
97-
guard let value, let binding = value.binding else {
98-
sqlite3_result_null(context)
99-
return
100-
}
101-
switch binding {
102-
case let integer as Int64:
103-
sqlite3_result_int64(context, integer)
104-
case let double as Double:
105-
sqlite3_result_double(context, double)
106-
case let text as String:
107-
sqlite3_result_text(context, text, -1, transientDestructor)
108-
case let blob as Blob:
109-
sqlite3_result_blob(context, blob.bytes, Int32(blob.bytes.count), transientDestructor)
110-
default:
111-
sqlite3_result_null(context)
31+
private extension AttributeValue {
32+
33+
/// Read a SQL function argument into an ``AttributeValue``, inferring its shape from
34+
/// the value's storage class.
35+
init(functionArgument binding: Binding) {
36+
switch binding {
37+
case let .integer(value):
38+
self = .int64(value)
39+
case let .double(value):
40+
self = .double(value)
41+
case let .text(value):
42+
self = .string(value)
43+
case .blob:
44+
self = .data(Data(binding.bytes ?? []))
45+
case .null:
46+
self = .null
47+
}
11248
}
11349
}

0 commit comments

Comments
 (0)