From a842943ad86c38efdf304c4a8afff14808a29f30 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 16 Jul 2026 20:56:44 -0400 Subject: [PATCH 1/4] Update dependencies --- Package.swift | 35 ++++++----------------------------- 1 file changed, 6 insertions(+), 29 deletions(-) diff --git a/Package.swift b/Package.swift index 7b9ac32..de12086 100644 --- a/Package.swift +++ b/Package.swift @@ -2,21 +2,6 @@ import PackageDescription import Foundation -// Android has no system libsqlite3, so it needs SQLite.swift's embedded -// copy. macOS and Linux keep SQLite.swift's own default for the platform. -let isAndroid = ProcessInfo.processInfo.environment["TARGET_OS_ANDROID"] == "1" - -let sqliteDependency: Package.Dependency = isAndroid - ? .package( - url: "https://github.com/stephencelis/SQLite.swift.git", - from: "0.16.0", - traits: ["SQLiteSwiftCSQLite"] - ) - : .package( - url: "https://github.com/stephencelis/SQLite.swift.git", - from: "0.16.0" - ) - let package = Package( name: "CoreModel-SQLite", platforms: [ @@ -36,13 +21,12 @@ let package = Package( url: "https://github.com/PureSwift/CoreModel", from: "2.8.0" ), - sqliteDependency, - // Off Apple platforms, SQLite.swift links the embedded SQLite from this package. - // We depend on it directly so we can call the SQLite C API (custom functions) - // where the system `SQLite3` module isn't available. + // PureSwift/SQLite wraps the system SQLite3 on Apple platforms and links + // an embedded SQLite (via swift-sqlcipher) everywhere else, so no separate + // C SQLite dependency or per-platform branching is needed here. .package( - url: "https://github.com/stephencelis/CSQLite", - from: "3.50.4" + url: "https://github.com/PureSwift/SQLite", + branch: "master" ) ], targets: [ @@ -52,14 +36,7 @@ let package = Package( "CoreModel", .product( name: "SQLite", - package: "SQLite.swift" - ), - // On Apple platforms the SQLite C API comes from the system `SQLite3` - // module; elsewhere it comes from SQLite.swift's embedded copy. - .product( - name: "SQLiteSwiftCSQLite", - package: "CSQLite", - condition: .when(platforms: [.linux, .android]) + package: "SQLite" ) ] ), From b574247d0f7eee1961967a5224142e4a86199765 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 16 Jul 2026 20:56:54 -0400 Subject: [PATCH 2/4] Update SQLite usage --- Sources/CoreModelSQLite/AttributeValue.swift | 81 ++++++------ Sources/CoreModelSQLite/CustomFunction.swift | 118 ++++-------------- Sources/CoreModelSQLite/Database.swift | 26 ++-- .../CoreModelSQLite/EntityDescription.swift | 2 +- Sources/CoreModelSQLite/ModelData.swift | 24 +--- Sources/CoreModelSQLite/Predicate.swift | 6 +- Sources/CoreModelSQLite/Relationship.swift | 20 +-- Sources/CoreModelSQLite/ViewContext.swift | 6 +- 8 files changed, 103 insertions(+), 180 deletions(-) diff --git a/Sources/CoreModelSQLite/AttributeValue.swift b/Sources/CoreModelSQLite/AttributeValue.swift index 4681e5d..2180595 100644 --- a/Sources/CoreModelSQLite/AttributeValue.swift +++ b/Sources/CoreModelSQLite/AttributeValue.swift @@ -12,34 +12,36 @@ import SQLite internal extension AttributeValue { /// Convert to a SQLite binding value. + /// + /// `nil` represents a SQL `NULL`. var binding: Binding? { switch self { case .null: return nil case let .string(value): - return value + return .text(value) case let .uuid(value): - return value.uuidString + return .text(value.uuidString) case let .url(value): - return value.absoluteString + return .text(value.absoluteString) case let .data(value): - return Blob(bytes: [UInt8](value)) + return Blob(bytes: [UInt8](value)).binding case let .date(value): - return value.timeIntervalSince1970 + return .double(value.timeIntervalSince1970) case let .bool(value): - return Int64(value ? 1 : 0) + return .integer(value ? 1 : 0) case let .int16(value): - return Int64(value) + return .integer(Int64(value)) case let .int32(value): - return Int64(value) + return .integer(Int64(value)) case let .int64(value): - return value + return .integer(value) case let .float(value): - return Double(value) + return .double(Double(value)) case let .double(value): - return value + return .double(value) case let .decimal(value): - return value.description + return .text(value.description) } } @@ -81,27 +83,27 @@ internal extension AttributeValue { } self = .double(value) case .string: - guard let value = binding as? String else { + guard let value = binding.textValue else { throw SQLiteDatabaseError.invalidBinding(binding, type) } self = .string(value) case .data: - guard let value = binding as? Blob else { + guard let value = binding.blobValue else { throw SQLiteDatabaseError.invalidBinding(binding, type) } - self = .data(Data(value.bytes)) + self = .data(Data(value)) case .date: guard let value = binding.doubleValue else { throw SQLiteDatabaseError.invalidBinding(binding, type) } self = .date(Date(timeIntervalSince1970: value)) case .uuid: - guard let string = binding as? String, let value = UUID(uuidString: string) else { + guard let string = binding.textValue, let value = UUID(uuidString: string) else { throw SQLiteDatabaseError.invalidBinding(binding, type) } self = .uuid(value) case .url: - guard let string = binding as? String, let value = URL(string: string) else { + guard let string = binding.textValue, let value = URL(string: string) else { throw SQLiteDatabaseError.invalidBinding(binding, type) } self = .url(value) @@ -114,43 +116,44 @@ internal extension AttributeValue { } } -private extension Binding { +internal extension Binding { + /// The integer value, converting `REAL` and `TEXT` where possible. var int64Value: Int64? { - switch self { - case let value as Int64: - return value - case let value as Double: - return Int64(exactly: value) - case let value as String: - return Int64(value) - default: + integer + } + + /// The floating-point value, converting `INTEGER` and `TEXT` where possible. + var doubleValue: Double? { + double + } + + /// The stored text value, without converting numeric storage classes to text. + var textValue: String? { + guard case let .text(value) = self else { return nil } + return value } - var doubleValue: Double? { - switch self { - case let value as Double: - return value - case let value as Int64: - return Double(value) - case let value as String: - return Double(value) - default: + /// The stored blob bytes, only for `BLOB` values. + var blobValue: [UInt8]? { + guard case .blob = self else { return nil } + return bytes } + /// The decimal value, parsed from `TEXT` or converted from a numeric storage class. var decimalValue: Decimal? { switch self { - case let value as String: + case let .text(value): return Decimal(string: value) - case let value as Double: + case let .double(value): return Decimal(value) - case let value as Int64: + case let .integer(value): return Decimal(value) - default: + case .blob, .null: return nil } } diff --git a/Sources/CoreModelSQLite/CustomFunction.swift b/Sources/CoreModelSQLite/CustomFunction.swift index 4019ebb..f456421 100644 --- a/Sources/CoreModelSQLite/CustomFunction.swift +++ b/Sources/CoreModelSQLite/CustomFunction.swift @@ -8,106 +8,42 @@ import Foundation import CoreModel import SQLite -#if canImport(Darwin) -import SQLite3 -#elseif canImport(SQLiteSwiftCSQLite) -import SQLiteSwiftCSQLite -#elseif canImport(CSQLite) -import CSQLite -#else -import SQLite3 -#endif - -// Registers custom scalar functions by calling `sqlite3_create_function_v2` directly -// with `@convention(c)` function pointers, rather than SQLite.swift's `createFunction`. -// SQLite.swift registers the callback with a `@convention(block)` closure cast to a raw -// pointer, which is unreliable off Apple platforms (upstream -// https://github.com/stephencelis/SQLite.swift/issues/1071). A plain C function pointer -// plus a retained context pointer works identically on every platform. - -/// The SQLite `SQLITE_TRANSIENT` sentinel destructor, telling SQLite to copy a result -/// value immediately (it is a macro in C, so it isn't imported). -private let transientDestructor = unsafeBitCast(-1, to: sqlite3_destructor_type.self) - -/// Retains a ``DatabaseFunction`` so it can be passed through SQLite as an opaque pointer -/// and recovered inside the C callback. -private final class FunctionBox { - let function: DatabaseFunction - init(_ function: DatabaseFunction) { self.function = function } -} internal extension SQLite.Connection { - /// Registers a `DatabaseFunction` with this connection via the SQLite C API. + /// Registers a `DatabaseFunction` as a custom SQL scalar function. func register(function: DatabaseFunction) throws { - let box = Unmanaged.passRetained(FunctionBox(function)) - let flags = SQLITE_UTF8 | (function.deterministic ? SQLITE_DETERMINISTIC : 0) - let argumentCount = function.argumentCount.map { Int32($0) } ?? -1 - let code = sqlite3_create_function_v2( - handle, + let evaluate = function.evaluate + try createFunction( function.name, - argumentCount, - flags, - box.toOpaque(), - { context, argc, argv in - let function = Unmanaged.fromOpaque(sqlite3_user_data(context)).takeUnretainedValue().function - var arguments = [AttributeValue?]() - arguments.reserveCapacity(Int(argc)) - for index in 0...fromOpaque(pointer).release() + argumentCount: function.argumentCount.map { Int32($0) }, + deterministic: function.deterministic + ) { arguments in + let values: [AttributeValue?] = arguments.map { AttributeValue(functionArgument: $0) } + guard let result = evaluate(values), let binding = result.binding else { + return .null } - ) - guard code == SQLITE_OK else { - box.release() // xDestroy isn't called when registration fails - throw SQLiteDatabaseError.unableToCreateFunction(function.name, code) + return binding } } } -/// Read a SQLite argument value into an ``AttributeValue``, inferring its shape from the -/// value's runtime storage class. -private func argumentValue(_ value: OpaquePointer?) -> AttributeValue { - switch sqlite3_value_type(value) { - case SQLITE_INTEGER: - return .int64(sqlite3_value_int64(value)) - case SQLITE_FLOAT: - return .double(sqlite3_value_double(value)) - case SQLITE_TEXT: - guard let text = sqlite3_value_text(value) else { return .null } - return .string(String(cString: text)) - case SQLITE_BLOB: - guard let bytes = sqlite3_value_blob(value) else { return .data(Data()) } - return .data(Data(bytes: bytes, count: Int(sqlite3_value_bytes(value)))) - default: - return .null - } -} - -/// Set a function's result on the SQLite context from an ``AttributeValue``. -private func setResult(_ context: OpaquePointer?, _ value: AttributeValue?) { - guard let value, let binding = value.binding else { - sqlite3_result_null(context) - return - } - switch binding { - case let integer as Int64: - sqlite3_result_int64(context, integer) - case let double as Double: - sqlite3_result_double(context, double) - case let text as String: - sqlite3_result_text(context, text, -1, transientDestructor) - case let blob as Blob: - sqlite3_result_blob(context, blob.bytes, Int32(blob.bytes.count), transientDestructor) - default: - sqlite3_result_null(context) +private extension AttributeValue { + + /// Read a SQL function argument into an ``AttributeValue``, inferring its shape from + /// the value's storage class. + init(functionArgument binding: Binding) { + switch binding { + case let .integer(value): + self = .int64(value) + case let .double(value): + self = .double(value) + case let .text(value): + self = .string(value) + case .blob: + self = .data(Data(binding.bytes ?? [])) + case .null: + self = .null + } } } diff --git a/Sources/CoreModelSQLite/Database.swift b/Sources/CoreModelSQLite/Database.swift index 5be7b4e..f6e9909 100644 --- a/Sources/CoreModelSQLite/Database.swift +++ b/Sources/CoreModelSQLite/Database.swift @@ -36,12 +36,12 @@ public actor SQLiteDatabase { /// file and, being read-only, can never create the schema itself; without this, a /// fetch through the view context before any write happens throws "no such table". public init( - connection: SQLite.Connection, + connection: consuming SQLite.Connection, model: Model ) throws { - self.connection = connection self.model = model try connection.createTables(model: model) + self.connection = connection } } @@ -49,7 +49,7 @@ public extension SQLiteDatabase { /// Open or create a database file at the specified path. init(path: String, model: Model) throws { - let connection = try Connection(path) + let connection = try Connection(path: path) try self.init(connection: connection, model: model) } } @@ -178,7 +178,7 @@ internal extension Connection { func fetch(_ entity: EntityName, for id: ObjectID, model: Model) throws -> ModelData? { let entityDescription = try model.entity(entity) let sql = "SELECT * FROM \(entity.rawValue.quotedIdentifier) WHERE \(SQLiteDatabase.primaryKeyColumn.quotedIdentifier) = ?" - let statement = try prepare(sql, [id.rawValue]) + let statement = try prepare(sql, [id.rawValue.binding]) guard let row = try statement.rowDictionaries().first else { return nil } @@ -210,7 +210,7 @@ internal extension Connection { let statement = try prepare(query.sql, query.bindings) var results = [ObjectID]() while let row = try statement.failableNext() { - guard let value = row[0] as? String else { continue } + guard let value = row[0]?.textValue else { continue } results.append(ObjectID(rawValue: value)) } return results @@ -219,7 +219,7 @@ internal extension Connection { func count(_ fetchRequest: FetchRequest, model: Model) throws -> UInt { let entityDescription = try model.entity(fetchRequest.entity) let query = try fetchRequest.sqlFragment(for: entityDescription, model: model, columns: "COUNT(*)") - guard let count = try scalar(query.sql, query.bindings) as? Int64 else { + guard let count = try scalar(query.sql, query.bindings)?.integer else { return 0 } return UInt(count) @@ -250,7 +250,7 @@ internal extension Connection { case .toOne: // one/many-to-many: nullify the foreign key on the destination table let sql = "UPDATE \(relationship.destinationEntity.rawValue.quotedIdentifier) SET \(relationship.inverseRelationship.rawValue.quotedIdentifier) = NULL WHERE \(relationship.inverseRelationship.rawValue.quotedIdentifier) = ?" - try run(sql, [id.rawValue]) + try run(sql, [id.rawValue.binding]) case .toMany: // many-to-many: drop this row's links from the join table let joinTable = JoinTable(entity: entity, relationship: relationship) @@ -260,12 +260,12 @@ internal extension Connection { // only a one-to-one inverse (the other table holding a back-reference) needs explicit nullify if try model.inverseType(of: relationship) == .toOne { let sql = "UPDATE \(relationship.destinationEntity.rawValue.quotedIdentifier) SET \(relationship.inverseRelationship.rawValue.quotedIdentifier) = NULL WHERE \(relationship.inverseRelationship.rawValue.quotedIdentifier) = ?" - try run(sql, [id.rawValue]) + try run(sql, [id.rawValue.binding]) } } } let sql = "DELETE FROM \(entity.rawValue.quotedIdentifier) WHERE \(SQLiteDatabase.primaryKeyColumn.quotedIdentifier) = ?" - try run(sql, [id.rawValue]) + try run(sql, [id.rawValue.binding]) } } @@ -320,10 +320,10 @@ internal extension Connection { // one-to-many: rewrite the foreign key on the destination table let table = relationship.destinationEntity.rawValue.quotedIdentifier let foreignKey = relationship.inverseRelationship.rawValue.quotedIdentifier - try run("UPDATE \(table) SET \(foreignKey) = NULL WHERE \(foreignKey) = ?", [value.id.rawValue]) + try run("UPDATE \(table) SET \(foreignKey) = NULL WHERE \(foreignKey) = ?", [value.id.rawValue.binding]) if destinationIDs.isEmpty == false { let placeholders = repeatElement("?", count: destinationIDs.count).joined(separator: ", ") - let bindings: [Binding?] = [value.id.rawValue] + destinationIDs.map { $0.rawValue } + let bindings: [Binding?] = [value.id.rawValue.binding] + destinationIDs.map { $0.rawValue.binding } try run("UPDATE \(table) SET \(foreignKey) = ? WHERE \(SQLiteDatabase.primaryKeyColumn.quotedIdentifier) IN (\(placeholders))", bindings) } case .toMany: @@ -340,10 +340,10 @@ internal extension Connection { switch try model.inverseType(of: relationship) { case .toOne: let sql = "SELECT \(SQLiteDatabase.primaryKeyColumn.quotedIdentifier) FROM \(relationship.destinationEntity.rawValue.quotedIdentifier) WHERE \(relationship.inverseRelationship.rawValue.quotedIdentifier) = ?" - let statement = try prepare(sql, [value.id.rawValue]) + let statement = try prepare(sql, [value.id.rawValue.binding]) var results = [ObjectID]() while let row = try statement.failableNext() { - guard let idString = row[0] as? String else { continue } + guard let idString = row[0]?.textValue else { continue } results.append(ObjectID(rawValue: idString)) } destinationIDs = results diff --git a/Sources/CoreModelSQLite/EntityDescription.swift b/Sources/CoreModelSQLite/EntityDescription.swift index 72d7d1a..4c5b906 100644 --- a/Sources/CoreModelSQLite/EntityDescription.swift +++ b/Sources/CoreModelSQLite/EntityDescription.swift @@ -25,7 +25,7 @@ internal extension SchemaChanger { internal extension SchemaChanger.CreateTableDefinition { - func addColumns(_ entity: EntityDescription) { + mutating func addColumns(_ entity: EntityDescription) { // add ID column let id = ColumnDefinition( diff --git a/Sources/CoreModelSQLite/ModelData.swift b/Sources/CoreModelSQLite/ModelData.swift index 4acac97..8a76c6c 100644 --- a/Sources/CoreModelSQLite/ModelData.swift +++ b/Sources/CoreModelSQLite/ModelData.swift @@ -17,7 +17,7 @@ internal extension ModelData { /// foreign key columns. To-many relationships are not stored on this table. func columnValues(for entity: EntityDescription) throws -> [(column: String, binding: Binding?)] { assert(entity.id == self.entity) - var values: [(String, Binding?)] = [(SQLiteDatabase.primaryKeyColumn, id.rawValue)] + var values: [(String, Binding?)] = [(SQLiteDatabase.primaryKeyColumn, id.rawValue.binding)] values.reserveCapacity(1 + entity.attributes.count + entity.relationships.count) for attribute in entity.attributes { let value = attributes[attribute.id] ?? .null @@ -29,7 +29,7 @@ internal extension ModelData { case .null: binding = nil case let .toOne(objectID): - binding = objectID.rawValue + binding = objectID.rawValue.binding case .toMany: throw SQLiteDatabaseError.invalidProperty(relationship.id, entity.id) } @@ -58,7 +58,7 @@ internal extension ModelData { /// to-many relationships require separate queries and are filled in by the database. init(row: [String: Binding?], entity: EntityDescription) throws { guard let idBinding = row[SQLiteDatabase.primaryKeyColumn] ?? nil, - let idString = idBinding as? String else { + let idString = idBinding.textValue else { throw SQLiteDatabaseError.invalidBinding(row[SQLiteDatabase.primaryKeyColumn] ?? nil, .string) } var attributes = [PropertyKey: AttributeValue]() @@ -71,7 +71,7 @@ internal extension ModelData { relationships.reserveCapacity(entity.relationships.count) for relationship in entity.relationships where relationship.type == .toOne { let binding = row[relationship.id.rawValue] ?? nil - if let string = binding as? String { + if let string = binding?.textValue { relationships[relationship.id] = .toOne(ObjectID(rawValue: string)) } else { relationships[relationship.id] = .null @@ -86,19 +86,3 @@ internal extension ModelData { } } -internal extension Statement { - - /// Iterate rows as dictionaries keyed by column name. - func rowDictionaries() throws -> [[String: Binding?]] { - let columnNames = self.columnNames - var results = [[String: Binding?]]() - while let row = try failableNext() { - var dictionary = [String: Binding?](minimumCapacity: columnNames.count) - for (index, name) in columnNames.enumerated() { - dictionary[name] = row[index] - } - results.append(dictionary) - } - return results - } -} diff --git a/Sources/CoreModelSQLite/Predicate.swift b/Sources/CoreModelSQLite/Predicate.swift index 0768cb5..0dfabad 100644 --- a/Sources/CoreModelSQLite/Predicate.swift +++ b/Sources/CoreModelSQLite/Predicate.swift @@ -232,7 +232,7 @@ internal extension FetchRequest.Predicate.Comparison { private extension SQLFragment { static func like(column: String, pattern: String) -> SQLFragment { - SQLFragment(sql: "\(column) LIKE ? ESCAPE '\\'", bindings: [pattern]) + SQLFragment(sql: "\(column) LIKE ? ESCAPE '\\'", bindings: [pattern.binding]) } } @@ -283,7 +283,7 @@ private extension FetchRequest.Predicate.Expression { case .null: return nil case let .toOne(objectID): - return objectID.rawValue + return objectID.rawValue.binding case .toMany: throw SQLiteDatabaseError.invalidPredicate(predicate) } @@ -296,7 +296,7 @@ private extension FetchRequest.Predicate.Expression { func constantBindings(predicate: FetchRequest.Predicate) throws -> [Binding?] { switch self { case let .relationship(.toMany(objectIDs)): - return objectIDs.map { $0.rawValue } + return objectIDs.map { $0.rawValue.binding } default: return [try constantBinding(predicate: predicate)] } diff --git a/Sources/CoreModelSQLite/Relationship.swift b/Sources/CoreModelSQLite/Relationship.swift index fba232f..a0ebb46 100644 --- a/Sources/CoreModelSQLite/Relationship.swift +++ b/Sources/CoreModelSQLite/Relationship.swift @@ -92,7 +92,7 @@ internal struct JoinTable { internal extension JoinTable { - func create(connection: Connection) throws { + func create(connection: borrowing Connection) throws { let sql = """ CREATE TABLE IF NOT EXISTS \(name.quotedIdentifier) ( \(thisColumn.quotedIdentifier) TEXT NOT NULL, @@ -104,38 +104,38 @@ internal extension JoinTable { } /// Fetch the object IDs linked to the specified object. - func fetch(_ id: ObjectID, connection: Connection) throws -> [ObjectID] { + func fetch(_ id: ObjectID, connection: borrowing Connection) throws -> [ObjectID] { var sql = "SELECT \(otherColumn.quotedIdentifier) FROM \(name.quotedIdentifier) WHERE \(thisColumn.quotedIdentifier) = ?" - var bindings: [Binding?] = [id.rawValue] + var bindings: [Binding?] = [id.rawValue.binding] if isSymmetric { sql += " UNION SELECT \(thisColumn.quotedIdentifier) FROM \(name.quotedIdentifier) WHERE \(otherColumn.quotedIdentifier) = ?" - bindings.append(id.rawValue) + bindings.append(id.rawValue.binding) } let statement = try connection.prepare(sql, bindings) var results = [ObjectID]() while let row = try statement.failableNext() { - guard let value = row[0] as? String else { continue } + guard let value = row[0]?.textValue else { continue } results.append(ObjectID(rawValue: value)) } return results } /// Replace all links of the specified object with the provided destination IDs. - func replace(_ id: ObjectID, with destinationIDs: [ObjectID], connection: Connection) throws { + func replace(_ id: ObjectID, with destinationIDs: [ObjectID], connection: borrowing Connection) throws { try removeAll(id, connection: connection) let sql = "INSERT OR IGNORE INTO \(name.quotedIdentifier) (\(thisColumn.quotedIdentifier), \(otherColumn.quotedIdentifier)) VALUES (?, ?)" for destination in destinationIDs { - try connection.run(sql, [id.rawValue, destination.rawValue]) + try connection.run(sql, [id.rawValue.binding, destination.rawValue.binding]) } } /// Remove all links involving the specified object. - func removeAll(_ id: ObjectID, connection: Connection) throws { + func removeAll(_ id: ObjectID, connection: borrowing Connection) throws { var sql = "DELETE FROM \(name.quotedIdentifier) WHERE \(thisColumn.quotedIdentifier) = ?" - var bindings: [Binding?] = [id.rawValue] + var bindings: [Binding?] = [id.rawValue.binding] if isSymmetric { sql += " OR \(otherColumn.quotedIdentifier) = ?" - bindings.append(id.rawValue) + bindings.append(id.rawValue.binding) } try connection.run(sql, bindings) } diff --git a/Sources/CoreModelSQLite/ViewContext.swift b/Sources/CoreModelSQLite/ViewContext.swift index 9af2b0d..d080174 100644 --- a/Sources/CoreModelSQLite/ViewContext.swift +++ b/Sources/CoreModelSQLite/ViewContext.swift @@ -23,14 +23,14 @@ public final class SQLiteViewContext: ViewContext { _ location: SQLite.Connection.Location, model: Model ) throws { - let connection = try SQLite.Connection.init(location, readonly: true) + let connection = try SQLite.Connection(path: location, isReadOnly: true) self.model = model self.connection = connection } - + /// Open or create a database file at the specified path. init(path: String, model: Model) throws { - let connection = try SQLite.Connection.init(path, readonly: true) + let connection = try SQLite.Connection(path: path, isReadOnly: true) self.model = model self.connection = connection } From a9468c20f64d3f29704d6bf3f012b236ab8f2903 Mon Sep 17 00:00:00 2001 From: Alsey Coleman Miller Date: Thu, 16 Jul 2026 21:11:04 -0400 Subject: [PATCH 3/4] Update unit tests --- .../CustomFunctionTests.swift | 18 ++++++++----- .../DistancePerformanceTests.swift | 26 +++++++++---------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/Tests/CoreModelSQLiteTests/CustomFunctionTests.swift b/Tests/CoreModelSQLiteTests/CustomFunctionTests.swift index 0a05ac8..5890500 100644 --- a/Tests/CoreModelSQLiteTests/CustomFunctionTests.swift +++ b/Tests/CoreModelSQLiteTests/CustomFunctionTests.swift @@ -63,9 +63,9 @@ private struct SeededGenerator: RandomNumberGenerator { /// Whether this SQLite build includes the R*Tree module (an optional compile-time /// feature), determined by attempting to create an R*Tree virtual table in memory. private let isRTreeAvailable: Bool = { - guard let connection = try? Connection(.inMemory) else { return false } + guard let connection = try? Connection(path: .inMemory) else { return false } do { - try connection.execute("CREATE VIRTUAL TABLE rtree_probe USING rtree(id, minX, maxX)") + try connection.run("CREATE VIRTUAL TABLE rtree_probe USING rtree(id, minX, maxX)") return true } catch { return false @@ -300,14 +300,18 @@ func appManagedRTreePrefilter() async throws { let maxLon = referenceLongitude + lonDelta // Query the app's R*Tree (through the app's own read connection) for candidate ids. - let reader = try Connection(path, readonly: true) - let candidates = Set(try reader.prepare(""" + let reader = try Connection(path: path, isReadOnly: true) + let statement = try reader.prepare(""" SELECT m.site_id FROM "Site_rtree" r JOIN "Site_rtree_map" m ON m.rowid = r.id WHERE r.minLat <= ? AND r.maxLat >= ? AND r.minLon <= ? AND r.maxLon >= ? - """, maxLat, minLat, maxLon, minLon).compactMap { row in - (row[0] as? String).map { ObjectID(rawValue: $0) } - }) + """, [maxLat.binding, minLat.binding, maxLon.binding, minLon.binding]) + var candidates = Set() + while let row = try statement.failableNext() { + if let site = row[0]?.textValue { + candidates.insert(ObjectID(rawValue: site)) + } + } // Combine the R*Tree candidate set with the exact distance filter via the library. let request = FetchRequest( diff --git a/Tests/CoreModelSQLiteTests/DistancePerformanceTests.swift b/Tests/CoreModelSQLiteTests/DistancePerformanceTests.swift index 9870740..846f10d 100644 --- a/Tests/CoreModelSQLiteTests/DistancePerformanceTests.swift +++ b/Tests/CoreModelSQLiteTests/DistancePerformanceTests.swift @@ -109,22 +109,21 @@ struct DistancePerformanceBenchmarks { #expect(try await database.fetch(sqlRequest).count == benchmarkInMemory(try await database.fetch(allRequest), radius: radius).count) - let clock = ContinuousClock() var sqlMatches = 0 - let sqlStart = clock.now + let sqlStart = Date() for _ in 0.. Date: Thu, 16 Jul 2026 21:15:06 -0400 Subject: [PATCH 4/4] Update GitHub CI --- .github/workflows/swift.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 2f67b4d..4c77769 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -38,6 +38,21 @@ jobs: - name: Test run: swift test -c ${{ matrix.config }} + windows: + runs-on: windows-latest + steps: + - uses: compnerd/gha-setup-swift@main + with: + branch: swift-6.3.2-release + tag: 6.3.2-RELEASE + - uses: actions/checkout@v4 + - name: Swift version + run: swift --version + - name: Build + run: swift build -v + - name: Test + run: swift test -v + skip-fuse: name: Skip Fuse (Android) runs-on: macos-26