Skip to content

Commit b574247

Browse files
committed
Update SQLite usage
1 parent a842943 commit b574247

8 files changed

Lines changed: 103 additions & 180 deletions

File tree

‎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
}

‎Sources/CoreModelSQLite/Database.swift‎

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -36,20 +36,20 @@ public actor SQLiteDatabase {
3636
/// file and, being read-only, can never create the schema itself; without this, a
3737
/// fetch through the view context before any write happens throws "no such table".
3838
public init(
39-
connection: SQLite.Connection,
39+
connection: consuming SQLite.Connection,
4040
model: Model
4141
) throws {
42-
self.connection = connection
4342
self.model = model
4443
try connection.createTables(model: model)
44+
self.connection = connection
4545
}
4646
}
4747

4848
public extension SQLiteDatabase {
4949

5050
/// Open or create a database file at the specified path.
5151
init(path: String, model: Model) throws {
52-
let connection = try Connection(path)
52+
let connection = try Connection(path: path)
5353
try self.init(connection: connection, model: model)
5454
}
5555
}
@@ -178,7 +178,7 @@ internal extension Connection {
178178
func fetch(_ entity: EntityName, for id: ObjectID, model: Model) throws -> ModelData? {
179179
let entityDescription = try model.entity(entity)
180180
let sql = "SELECT * FROM \(entity.rawValue.quotedIdentifier) WHERE \(SQLiteDatabase.primaryKeyColumn.quotedIdentifier) = ?"
181-
let statement = try prepare(sql, [id.rawValue])
181+
let statement = try prepare(sql, [id.rawValue.binding])
182182
guard let row = try statement.rowDictionaries().first else {
183183
return nil
184184
}
@@ -210,7 +210,7 @@ internal extension Connection {
210210
let statement = try prepare(query.sql, query.bindings)
211211
var results = [ObjectID]()
212212
while let row = try statement.failableNext() {
213-
guard let value = row[0] as? String else { continue }
213+
guard let value = row[0]?.textValue else { continue }
214214
results.append(ObjectID(rawValue: value))
215215
}
216216
return results
@@ -219,7 +219,7 @@ internal extension Connection {
219219
func count(_ fetchRequest: FetchRequest, model: Model) throws -> UInt {
220220
let entityDescription = try model.entity(fetchRequest.entity)
221221
let query = try fetchRequest.sqlFragment(for: entityDescription, model: model, columns: "COUNT(*)")
222-
guard let count = try scalar(query.sql, query.bindings) as? Int64 else {
222+
guard let count = try scalar(query.sql, query.bindings)?.integer else {
223223
return 0
224224
}
225225
return UInt(count)
@@ -250,7 +250,7 @@ internal extension Connection {
250250
case .toOne:
251251
// one/many-to-many: nullify the foreign key on the destination table
252252
let sql = "UPDATE \(relationship.destinationEntity.rawValue.quotedIdentifier) SET \(relationship.inverseRelationship.rawValue.quotedIdentifier) = NULL WHERE \(relationship.inverseRelationship.rawValue.quotedIdentifier) = ?"
253-
try run(sql, [id.rawValue])
253+
try run(sql, [id.rawValue.binding])
254254
case .toMany:
255255
// many-to-many: drop this row's links from the join table
256256
let joinTable = JoinTable(entity: entity, relationship: relationship)
@@ -260,12 +260,12 @@ internal extension Connection {
260260
// only a one-to-one inverse (the other table holding a back-reference) needs explicit nullify
261261
if try model.inverseType(of: relationship) == .toOne {
262262
let sql = "UPDATE \(relationship.destinationEntity.rawValue.quotedIdentifier) SET \(relationship.inverseRelationship.rawValue.quotedIdentifier) = NULL WHERE \(relationship.inverseRelationship.rawValue.quotedIdentifier) = ?"
263-
try run(sql, [id.rawValue])
263+
try run(sql, [id.rawValue.binding])
264264
}
265265
}
266266
}
267267
let sql = "DELETE FROM \(entity.rawValue.quotedIdentifier) WHERE \(SQLiteDatabase.primaryKeyColumn.quotedIdentifier) = ?"
268-
try run(sql, [id.rawValue])
268+
try run(sql, [id.rawValue.binding])
269269
}
270270
}
271271

@@ -320,10 +320,10 @@ internal extension Connection {
320320
// one-to-many: rewrite the foreign key on the destination table
321321
let table = relationship.destinationEntity.rawValue.quotedIdentifier
322322
let foreignKey = relationship.inverseRelationship.rawValue.quotedIdentifier
323-
try run("UPDATE \(table) SET \(foreignKey) = NULL WHERE \(foreignKey) = ?", [value.id.rawValue])
323+
try run("UPDATE \(table) SET \(foreignKey) = NULL WHERE \(foreignKey) = ?", [value.id.rawValue.binding])
324324
if destinationIDs.isEmpty == false {
325325
let placeholders = repeatElement("?", count: destinationIDs.count).joined(separator: ", ")
326-
let bindings: [Binding?] = [value.id.rawValue] + destinationIDs.map { $0.rawValue }
326+
let bindings: [Binding?] = [value.id.rawValue.binding] + destinationIDs.map { $0.rawValue.binding }
327327
try run("UPDATE \(table) SET \(foreignKey) = ? WHERE \(SQLiteDatabase.primaryKeyColumn.quotedIdentifier) IN (\(placeholders))", bindings)
328328
}
329329
case .toMany:
@@ -340,10 +340,10 @@ internal extension Connection {
340340
switch try model.inverseType(of: relationship) {
341341
case .toOne:
342342
let sql = "SELECT \(SQLiteDatabase.primaryKeyColumn.quotedIdentifier) FROM \(relationship.destinationEntity.rawValue.quotedIdentifier) WHERE \(relationship.inverseRelationship.rawValue.quotedIdentifier) = ?"
343-
let statement = try prepare(sql, [value.id.rawValue])
343+
let statement = try prepare(sql, [value.id.rawValue.binding])
344344
var results = [ObjectID]()
345345
while let row = try statement.failableNext() {
346-
guard let idString = row[0] as? String else { continue }
346+
guard let idString = row[0]?.textValue else { continue }
347347
results.append(ObjectID(rawValue: idString))
348348
}
349349
destinationIDs = results

‎Sources/CoreModelSQLite/EntityDescription.swift‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ internal extension SchemaChanger {
2525

2626
internal extension SchemaChanger.CreateTableDefinition {
2727

28-
func addColumns(_ entity: EntityDescription) {
28+
mutating func addColumns(_ entity: EntityDescription) {
2929

3030
// add ID column
3131
let id = ColumnDefinition(

0 commit comments

Comments
 (0)