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
12 changes: 12 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,31 @@ let package = Package(
.library(
name: "LSQLite",
targets: ["LSQLite"]
),
.library(
name: "LSQLiteExtensions",
targets: ["LSQLiteExtensions"]
)
],
targets: [
.target(
name: "LSQLite",
dependencies: ["MissedSwiftSQLite"]
),
.target(
name: "LSQLiteExtensions",
dependencies: ["LSQLite"]
),
.target(
name: "MissedSwiftSQLite"
),
.testTarget(
name: "LSQLiteTests",
dependencies: ["LSQLite", "MissedSwiftSQLite"]
),
.testTarget(
name: "LSQLiteExtensionsTests",
dependencies: ["LSQLiteExtensions"]
),
]
)
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

A zero-overhead, typed Swift wrapper around the SQLite C API — same functions, no `OpaquePointer`, no magic constants.

LSQLiteExtensions is an add-on target in this package that layers opt-in conveniences on top of LSQLite while keeping SQLite semantics intact. It focuses on reducing boilerplate for common workflows (for example, Codable binding and row decoding) without introducing higher-level abstractions or a throwing error model.

## Motivation

The SQLite C API is small and powerful, but in Swift it comes with a few pain points:
Expand Down
37 changes: 37 additions & 0 deletions Sources/LSQLiteExtensions/Coding/Binding/Statement+Binding.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import Foundation
import LSQLite

extension Statement {
/// Encodes and binds a value to the statement's named parameters.
/// - Parameter binding: Value to encode and bind.
/// - Returns: `true` when all parameters match and binding succeeds; otherwise `false`.
///
/// Only top-level keyed containers are supported. Coding keys must match the
/// statement parameter names after removing the leading ":" prefix. Keys
/// must be emitted for every parameter; optional values must encode `nil`
/// explicitly. Supported value types are `nil`, `Data`, `String`, `Int`
/// (64-bit), and `Double`.
///
/// Related SQLite: `sqlite3_bind_parameter_count`, `sqlite3_bind_parameter_name`, `sqlite3_bind_blob`, `sqlite3_bind_text`, `sqlite3_bind_int64`, `sqlite3_bind_double`, `sqlite3_bind_null`, `sqlite3_bind_zeroblob`
public func bind<Binding: Encodable>(_ binding: Binding) -> Bool {
do {
let parameterMap = try statementParameterMap(for: self)
let keyCollector = StatementKeyCollectorEncoder()
try binding.encode(to: keyCollector)
if keyCollector.failure != nil {
return false
}
if keyCollector.keys != Set(parameterMap.keys) {
return false
}
let encoder = StatementBindingEncoder(statement: self, parameterMap: parameterMap)
try binding.encode(to: encoder)
if encoder.failure != nil {
return false
}
return true
} catch {
return false
}
}
}
172 changes: 172 additions & 0 deletions Sources/LSQLiteExtensions/Coding/Binding/StatementBindingEncoder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import Foundation
import LSQLite

final class StatementBindingEncoder: Encoder {
var codingPath: [CodingKey] = []
var userInfo: [CodingUserInfoKey: Any] = [:]
var failure: StatementCodingFailure?
let statement: Statement
let parameterMap: [String: Int32]

init(statement: Statement, parameterMap: [String: Int32]) {
self.statement = statement
self.parameterMap = parameterMap
}

func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> {
return KeyedEncodingContainer(StatementBindingContainer(encoder: self))
}

func unkeyedContainer() -> UnkeyedEncodingContainer {
fail(.unsupportedContainer)
return StatementFailingUnkeyedEncodingContainer(codingPath: codingPath, failure: fail)
}

func singleValueContainer() -> SingleValueEncodingContainer {
fail(.unsupportedContainer)
return StatementFailingSingleValueEncodingContainer(codingPath: codingPath, failure: fail)
}

func fail(_ error: StatementCodingFailure) {
if failure == nil {
failure = error
}
}
}

struct StatementBindingContainer<Key: CodingKey>: KeyedEncodingContainerProtocol {
var codingPath: [CodingKey] { encoder.codingPath }
let encoder: StatementBindingEncoder

init(encoder: StatementBindingEncoder) {
self.encoder = encoder
}

mutating func encodeNil(forKey key: Key) throws {
let index = try parameterIndex(for: key)
try bindResult(encoder.statement.bindNull(at: index))
}

mutating func encode(_ value: Bool, forKey key: Key) throws {
try unsupportedValue()
}

mutating func encode(_ value: String, forKey key: Key) throws {
let index = try parameterIndex(for: key)
try bindResult(encoder.statement.bindText(value, at: index))
}

mutating func encode(_ value: Double, forKey key: Key) throws {
let index = try parameterIndex(for: key)
try bindResult(encoder.statement.bindDouble(value, at: index))
}

mutating func encode(_ value: Float, forKey key: Key) throws {
try unsupportedValue()
}

mutating func encode(_ value: Int, forKey key: Key) throws {
let index = try parameterIndex(for: key)
try bindResult(encoder.statement.bindInt64(Int64(value), at: index))
}

mutating func encode(_ value: Int8, forKey key: Key) throws {
try unsupportedValue()
}

mutating func encode(_ value: Int16, forKey key: Key) throws {
try unsupportedValue()
}

mutating func encode(_ value: Int32, forKey key: Key) throws {
try unsupportedValue()
}

mutating func encode(_ value: Int64, forKey key: Key) throws {
try unsupportedValue()
}

mutating func encode(_ value: UInt, forKey key: Key) throws {
try unsupportedValue()
}

mutating func encode(_ value: UInt8, forKey key: Key) throws {
try unsupportedValue()
}

mutating func encode(_ value: UInt16, forKey key: Key) throws {
try unsupportedValue()
}

mutating func encode(_ value: UInt32, forKey key: Key) throws {
try unsupportedValue()
}

mutating func encode(_ value: UInt64, forKey key: Key) throws {
try unsupportedValue()
}

mutating func encode<T: Encodable>(_ value: T, forKey key: Key) throws {
if let data = value as? Data {
let index = try parameterIndex(for: key)
try bindData(data, at: index)
return
}
try unsupportedValue()
}

mutating func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer<NestedKey> {
encoder.fail(.unsupportedContainer)
return KeyedEncodingContainer(StatementFailingKeyedEncodingContainer(codingPath: codingPath, failure: encoder.fail))
}

mutating func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer {
encoder.fail(.unsupportedContainer)
return StatementFailingUnkeyedEncodingContainer(codingPath: codingPath, failure: encoder.fail)
}

mutating func superEncoder() -> Encoder {
encoder.fail(.unsupportedContainer)
return StatementFailingEncoder(codingPath: codingPath, failure: encoder.fail)
}

mutating func superEncoder(forKey key: Key) -> Encoder {
encoder.fail(.unsupportedContainer)
return StatementFailingEncoder(codingPath: codingPath, failure: encoder.fail)
}

func parameterIndex(for key: Key) throws -> Int32 {
let name = key.stringValue
guard let index = encoder.parameterMap[name] else {
encoder.fail(.invalidParameter)
throw StatementCodingFailure.invalidParameter
}
return index
}

func bindResult(_ result: ResultCode) throws {
guard result == .ok else {
encoder.fail(.unsupportedValue)
throw StatementCodingFailure.unsupportedValue
}
}

func bindData(_ data: Data, at index: Int32) throws {
if data.isEmpty {
try bindResult(encoder.statement.bindZeroBlob(length: 0, at: index))
return
}
let result: ResultCode = data.withUnsafeBytes { buffer in
guard let baseAddress = buffer.baseAddress else {
return encoder.statement.bindZeroBlob(length: 0, at: index)
}
return encoder.statement.bindTransientBlob(baseAddress, length: Int32(buffer.count), at: index)
}
try bindResult(result)
}

func unsupportedValue() throws {
encoder.fail(.unsupportedValue)
throw StatementCodingFailure.unsupportedValue
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import Foundation

final class StatementKeyCollectorEncoder: Encoder {
var codingPath: [CodingKey] = []
var userInfo: [CodingUserInfoKey: Any] = [:]
var keys: Set<String> = []
var failure: StatementCodingFailure?

init() {}

func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> {
return KeyedEncodingContainer(StatementKeyCollectorContainer(encoder: self))
}

func unkeyedContainer() -> UnkeyedEncodingContainer {
fail(.unsupportedContainer)
return StatementFailingUnkeyedEncodingContainer(codingPath: codingPath, failure: fail)
}

func singleValueContainer() -> SingleValueEncodingContainer {
fail(.unsupportedContainer)
return StatementFailingSingleValueEncodingContainer(codingPath: codingPath, failure: fail)
}

func record<Key: CodingKey>(_ key: Key) {
keys.insert(key.stringValue)
}

func fail(_ error: StatementCodingFailure) {
if failure == nil {
failure = error
}
}
}

struct StatementKeyCollectorContainer<Key: CodingKey>: KeyedEncodingContainerProtocol {
var codingPath: [CodingKey] { encoder.codingPath }
let encoder: StatementKeyCollectorEncoder

init(encoder: StatementKeyCollectorEncoder) {
self.encoder = encoder
}

mutating func encodeNil(forKey key: Key) throws {
encoder.record(key)
}

mutating func encode<T: Encodable>(_ value: T, forKey key: Key) throws {
encoder.record(key)
}

mutating func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer<NestedKey> {
encoder.fail(.unsupportedContainer)
return KeyedEncodingContainer(StatementFailingKeyedEncodingContainer(codingPath: codingPath, failure: encoder.fail))
}

mutating func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer {
encoder.fail(.unsupportedContainer)
return StatementFailingUnkeyedEncodingContainer(codingPath: codingPath, failure: encoder.fail)
}

mutating func superEncoder() -> Encoder {
encoder.fail(.unsupportedContainer)
return StatementFailingEncoder(codingPath: codingPath, failure: encoder.fail)
}

mutating func superEncoder(forKey key: Key) -> Encoder {
encoder.fail(.unsupportedContainer)
return StatementFailingEncoder(codingPath: codingPath, failure: encoder.fail)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import LSQLite

func statementParameterMap(for statement: Statement) throws -> [String: Int32] {
let count = Int(statement.bindingCount)
guard count > 0 else {
return [:]
}
var map: [String: Int32] = [:]
map.reserveCapacity(count)
for index in 1...count {
let index32 = Int32(index)
guard let name = statement.bindingName(at: index32) else {
throw StatementCodingFailure.invalidParameter
}
guard name.hasPrefix(":") else {
throw StatementCodingFailure.invalidParameter
}
let trimmed = String(name.dropFirst())
guard !trimmed.isEmpty else {
throw StatementCodingFailure.invalidParameter
}
if map[trimmed] != nil {
throw StatementCodingFailure.duplicateParameter
}
map[trimmed] = index32
}
return map
}
Loading
Loading