Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/workflows/swift.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
35 changes: 6 additions & 29 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand All @@ -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: [
Expand All @@ -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"
)
]
),
Expand Down
81 changes: 42 additions & 39 deletions Sources/CoreModelSQLite/AttributeValue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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)
Expand All @@ -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
}
}
Expand Down
118 changes: 27 additions & 91 deletions Sources/CoreModelSQLite/CustomFunction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<FunctionBox>.fromOpaque(sqlite3_user_data(context)).takeUnretainedValue().function
var arguments = [AttributeValue?]()
arguments.reserveCapacity(Int(argc))
for index in 0..<Int(argc) {
arguments.append(argumentValue(argv?[index]))
}
setResult(context, function.evaluate(arguments))
},
nil, // xStep (scalar function, no aggregate)
nil, // xFinal
{ pointer in
// Balance `passRetained` when SQLite drops the function.
guard let pointer else { return }
Unmanaged<FunctionBox>.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
}
}
}
Loading
Loading