Skip to content

Commit 1bfcf1c

Browse files
Merge pull request #16 from Alexander-Ignition/swift-6.2
Swift 6.2
2 parents b8aa8c9 + 52730a8 commit 1bfcf1c

File tree

8 files changed

+118
-95
lines changed

8 files changed

+118
-95
lines changed

.github/workflows/test.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ on:
2424
jobs:
2525
Apple:
2626
name: Test ${{ matrix.name }}
27-
runs-on: macOS-latest
27+
runs-on: macOS-26
2828
strategy:
2929
fail-fast: false
3030
matrix:
@@ -41,7 +41,7 @@ jobs:
4141
shell: bash
4242
SPM:
4343
name: Test with SPM
44-
runs-on: macOS-latest
44+
runs-on: macos-26
4545
steps:
4646
- name: Checkout
4747
uses: actions/checkout@v4

Makefile

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,16 @@ test:
2727
test-macos: $(OUTPUD_DIR)/test-macos.xcresult
2828
test-ios: $(OUTPUD_DIR)/test-ios.xcresult
2929

30-
XCODEBUILD_TEST = xcodebuild test -quiet -scheme $(TARGET_NAME)
31-
XCCOV = xcrun xccov view --files-for-target $(TARGET_NAME)
30+
XCODEBUILD_TEST = xcodebuild test -quiet -scheme $(TARGET_NAME) -resultBundlePath $@
31+
XCCOV = xcrun xccov view --files-for-target $(TARGET_NAME) --report $@
3232

3333
$(OUTPUD_DIR)/test-macos.xcresult:
34-
$(XCODEBUILD_TEST) -destination 'platform=macOS' -resultBundlePath $@
35-
$(XCCOV) --report $@
34+
$(XCODEBUILD_TEST) -destination 'platform=macOS'
35+
$(XCCOV)
3636

3737
$(OUTPUD_DIR)/test-ios.xcresult:
38-
$(XCODEBUILD_TEST) -destination 'platform=iOS Simulator,name=iPhone 16' -resultBundlePath $@
39-
$(XCCOV) --report $@
38+
$(XCODEBUILD_TEST) -destination 'platform=iOS Simulator,name=iPhone 17'
39+
$(XCCOV)
4040

4141
# Apple Containerization or Docker
4242
CONTAINER ?= container

Package.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// swift-tools-version:5.9
1+
// swift-tools-version:6.2
22
// The swift-tools-version declares the minimum version of Swift required to build this package.
33

44
import PackageDescription
@@ -30,7 +30,11 @@ let package = Package(
3030
),
3131
.systemLibrary(
3232
name: "SQLite3",
33-
pkgConfig: "sqlite3"
33+
pkgConfig: "sqlite3",
34+
providers: [
35+
.apt(["libsqlite3-dev"]),
36+
.brew(["sqlite3"]),
37+
]
3438
),
3539
.testTarget(
3640
name: "SQLyraTests",

Sources/SQLyra/DataFrame.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ extension PreparedStatement {
4545
while let row = try row() {
4646
df.appendEmptyRow()
4747
for index in (0..<columnCount) {
48-
df.rows[count][index] = row[index].flatMap { valueTransformers[index].transform($0) }
48+
if let value = row[index] {
49+
df.rows[count][index] = valueTransformers[index].transform(value)
50+
}
4951
}
5052
count += 1
5153
}
@@ -73,10 +75,10 @@ public struct ColumnValueTransformer: Sendable {
7375
@usableFromInline
7476
let column: @Sendable (_ name: String, _ capacity: Int) -> AnyColumn
7577
@usableFromInline
76-
let transform: @Sendable (PreparedStatement.Value) -> Any?
78+
let transform: @Sendable (borrowing PreparedStatement.Value) -> Any?
7779

7880
@inlinable
79-
public init<T>(transform: @escaping @Sendable (PreparedStatement.Value) -> T?) {
81+
public init<T>(transform: @escaping @Sendable (borrowing PreparedStatement.Value) -> T?) {
8082
self.column = { name, capacity in
8183
TabularData.Column<T>(name: name, capacity: capacity).eraseToAnyColumn()
8284
}

Sources/SQLyra/PreparedStatement.swift

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -201,62 +201,73 @@ extension PreparedStatement {
201201
}
202202

203203
@dynamicMemberLookup
204-
public struct Row {
204+
public struct Row: ~Copyable {
205205
let statement: PreparedStatement
206206

207207
public subscript(dynamicMember name: String) -> Value? {
208208
self[name]
209209
}
210210

211-
public subscript(name: String) -> Value? {
212-
statement.columnIndexByName[name].flatMap { self[$0] }
211+
public subscript(columnName: String) -> Value? {
212+
statement.columnIndexByName[columnName].flatMap { self[$0] }
213213
}
214214

215-
public subscript(index: Int) -> Value? {
216-
if sqlite3_column_type(statement.stmt, Int32(index)) == SQLITE_NULL {
217-
return nil
218-
}
219-
return Value(index: Int32(index), statement: statement)
215+
public subscript(columnIndex: Int) -> Value? {
216+
statement.value(at: columnIndex)
217+
}
218+
219+
public func decode<T>(_ type: T.Type, using decoder: RowDecoder? = nil) throws -> T where T: Decodable {
220+
try (decoder ?? RowDecoder.default).decode(type, from: self)
220221
}
222+
}
221223

222-
public func decode<T>(_ type: T.Type) throws -> T where T: Decodable {
223-
try decode(type, using: RowDecoder.default)
224+
func null(for columnName: String) -> Bool {
225+
guard let columnIndex = columnIndexByName[columnName] else {
226+
return true
224227
}
228+
return null(at: columnIndex)
229+
}
230+
231+
private func null(at columnIndex: Int) -> Bool {
232+
sqlite3_column_type(stmt, Int32(columnIndex)) == SQLITE_NULL
233+
}
225234

226-
public func decode<T>(_ type: T.Type, using decoder: RowDecoder) throws -> T where T: Decodable {
227-
try decoder.decode(type, from: self)
235+
func value(at columnIndex: Int) -> Value? {
236+
if null(at: columnIndex) {
237+
return nil
228238
}
239+
return Value(columnIndex: Int32(columnIndex), statement: self)
229240
}
230241

231242
/// Result value from a query.
232-
public struct Value {
233-
let index: Int32
243+
public struct Value: ~Copyable {
244+
let columnIndex: Int32
234245
let statement: PreparedStatement
235246
private var stmt: OpaquePointer { statement.stmt }
236247

237248
/// 64-bit INTEGER result.
238-
public var int64: Int64 { sqlite3_column_int64(stmt, index) }
249+
public var int64: Int64 { sqlite3_column_int64(stmt, columnIndex) }
239250

240251
/// 32-bit INTEGER result.
241-
public var int32: Int32 { sqlite3_column_int(stmt, index) }
252+
public var int32: Int32 { sqlite3_column_int(stmt, columnIndex) }
242253

243254
/// A platform-specific integer.
244255
public var int: Int { Int(int64) }
245256

246257
/// 64-bit IEEE floating point number.
247-
public var double: Double { sqlite3_column_double(stmt, index) }
258+
public var double: Double { sqlite3_column_double(stmt, columnIndex) }
248259

249260
/// Size of a BLOB or a UTF-8 TEXT result in bytes.
250-
public var count: Int { Int(sqlite3_column_bytes(stmt, index)) }
261+
public var count: Int { Int(sqlite3_column_bytes(stmt, columnIndex)) }
251262

252263
/// UTF-8 TEXT result.
253264
public var string: String? {
254-
sqlite3_column_text(stmt, index).flatMap { String(cString: $0) }
265+
sqlite3_column_text(stmt, columnIndex).flatMap { String(cString: $0) }
255266
}
256267

257268
/// BLOB result.
258269
public var blob: Data? {
259-
sqlite3_column_blob(stmt, index).map { Data(bytes: $0, count: count) }
270+
sqlite3_column_blob(stmt, columnIndex).map { Data(bytes: $0, count: count) }
260271
}
261272
}
262273
}

Sources/SQLyra/RowDecoder.swift

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import SQLite3
2-
31
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
42
import Foundation
53
#else
@@ -16,14 +14,22 @@ public final class RowDecoder {
1614
/// Creates a new, reusable row decoder.
1715
public init() {}
1816

19-
public func decode<T>(_ type: T.Type, from row: PreparedStatement.Row) throws -> T where T: Decodable {
20-
let decoder = _RowDecoder(row: row, userInfo: userInfo)
17+
public func decode<T>(
18+
_ type: T.Type,
19+
from row: borrowing PreparedStatement.Row
20+
) throws -> T where T: Decodable {
21+
let decoder = _RowDecoder(statement: row.statement, userInfo: userInfo)
2122
return try type.init(from: decoder)
2223
}
2324
}
2425

25-
private struct _RowDecoder: Decoder {
26-
let row: PreparedStatement.Row
26+
private final class _RowDecoder: Decoder {
27+
let statement: PreparedStatement
28+
29+
init(statement: PreparedStatement, userInfo: [CodingUserInfoKey: Any]) {
30+
self.statement = statement
31+
self.userInfo = userInfo
32+
}
2733

2834
// MARK: - Decoder
2935

@@ -47,9 +53,9 @@ private struct _RowDecoder: Decoder {
4753
struct KeyedContainer<Key: CodingKey>: KeyedDecodingContainerProtocol {
4854
let decoder: _RowDecoder
4955
var codingPath: [any CodingKey] { decoder.codingPath }
50-
var allKeys: [Key] { decoder.row.statement.columnIndexByName.keys.compactMap { Key(stringValue: $0) } }
56+
var allKeys: [Key] { decoder.statement.columnIndexByName.keys.compactMap { Key(stringValue: $0) } }
5157

52-
func contains(_ key: Key) -> Bool { decoder.row.statement.columnIndexByName.keys.contains(key.stringValue) }
58+
func contains(_ key: Key) -> Bool { decoder.statement.columnIndexByName.keys.contains(key.stringValue) }
5359
func decodeNil(forKey key: Key) throws -> Bool { decoder.null(for: key) }
5460
func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool { try decoder.bool(forKey: key) }
5561
func decode(_ type: String.Type, forKey key: Key) throws -> String { try decoder.string(forKey: key) }
@@ -84,7 +90,7 @@ private struct _RowDecoder: Decoder {
8490

8591
@inline(__always)
8692
func null<K>(for key: K) -> Bool where K: CodingKey {
87-
row[key.stringValue] == nil
93+
statement.null(for: key.stringValue)
8894
}
8995

9096
@inline(__always)
@@ -131,13 +137,13 @@ private struct _RowDecoder: Decoder {
131137

132138
@inline(__always)
133139
private func columnValue<T, K>(_ type: T.Type, forKey key: K) throws -> PreparedStatement.Value where K: CodingKey {
134-
guard let index = row.statement.columnIndexByName[key.stringValue] else {
140+
guard let index = statement.columnIndexByName[key.stringValue] else {
135141
throw DecodingError.keyNotFound(key, .context([key], "Column index not found for key: \(key)"))
136142
}
137-
guard let column = row[index] else {
143+
guard let value = statement.value(at: index) else {
138144
throw DecodingError.valueNotFound(type, .context([key], "Column value not found for key: \(key)"))
139145
}
140-
return column
146+
return value
141147
}
142148
}
143149

@@ -151,11 +157,11 @@ private struct _ValueDecoder: Decoder, SingleValueDecodingContainer {
151157
var codingPath: [any CodingKey] { [key] }
152158

153159
func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> where Key: CodingKey {
154-
throw DecodingError.typeMismatch(PreparedStatement.Value.self, .context(codingPath, ""))
160+
throw DecodingError.dataCorrupted(.context(codingPath, ""))
155161
}
156162

157163
func unkeyedContainer() throws -> any UnkeyedDecodingContainer {
158-
throw DecodingError.typeMismatch(PreparedStatement.Value.self, .context(codingPath, ""))
164+
throw DecodingError.dataCorrupted(.context(codingPath, ""))
159165
}
160166

161167
func singleValueContainer() throws -> any SingleValueDecodingContainer {

Tests/SQLyraTests/DatabaseTests.swift

Lines changed: 35 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,66 +8,67 @@ import Foundation
88
import FoundationEssentials
99
#endif
1010

11-
@Test func openOptionsRawValue() {
12-
typealias Options = Database.OpenOptions
13-
#expect(Options.create.rawValue == SQLITE_OPEN_CREATE)
14-
#expect(Options.readwrite.rawValue == SQLITE_OPEN_READWRITE)
15-
#expect(Options.readonly.rawValue == SQLITE_OPEN_READONLY)
16-
#expect(Options.memory.rawValue == SQLITE_OPEN_MEMORY)
17-
#expect(Options.extendedResultCode.rawValue == SQLITE_OPEN_EXRESCODE)
18-
#expect(Options.uri.rawValue == SQLITE_OPEN_URI)
19-
#expect(Options.noFollow.rawValue == SQLITE_OPEN_NOFOLLOW)
20-
#expect(Options.noMutex.rawValue == SQLITE_OPEN_NOMUTEX)
21-
#expect(Options.fullMutex.rawValue == SQLITE_OPEN_FULLMUTEX)
22-
#expect(Options.sharedCache.rawValue == SQLITE_OPEN_SHAREDCACHE)
23-
#expect(Options.privateCache.rawValue == SQLITE_OPEN_PRIVATECACHE)
11+
extension Database.OpenOptions: CustomTestStringConvertible {
12+
public var testDescription: String {
13+
String(rawValue, radix: 16, uppercase: true)
14+
}
2415
}
2516

2617
struct DatabaseTests {
27-
private let fileManager = FileManager.default
28-
private let path = "Tests/new.db"
2918

30-
init() {
31-
#if Xcode // for relative path
32-
fileManager.changeCurrentDirectoryPath(#file.components(separatedBy: "/Tests")[0])
33-
#endif
19+
@Test(arguments: [
20+
(Database.OpenOptions.create, SQLITE_OPEN_CREATE),
21+
(Database.OpenOptions.readwrite, SQLITE_OPEN_READWRITE),
22+
(Database.OpenOptions.readonly, SQLITE_OPEN_READONLY),
23+
(Database.OpenOptions.memory, SQLITE_OPEN_MEMORY),
24+
(Database.OpenOptions.extendedResultCode, SQLITE_OPEN_EXRESCODE),
25+
(Database.OpenOptions.uri, SQLITE_OPEN_URI),
26+
(Database.OpenOptions.noFollow, SQLITE_OPEN_NOFOLLOW),
27+
(Database.OpenOptions.noMutex, SQLITE_OPEN_NOMUTEX),
28+
(Database.OpenOptions.fullMutex, SQLITE_OPEN_FULLMUTEX),
29+
(Database.OpenOptions.sharedCache, SQLITE_OPEN_SHAREDCACHE),
30+
(Database.OpenOptions.privateCache, SQLITE_OPEN_PRIVATECACHE),
31+
])
32+
func open(options: Database.OpenOptions, expected: Int32) {
33+
#expect(options.rawValue == expected)
3434
}
3535

3636
@Test func open() throws {
37-
let url = URL(fileURLWithPath: path)
38-
var database: Database! = try Database.open(at: path, options: [.readwrite, .create])
37+
let fileManager = FileManager.default
38+
try #require(fileManager.changeCurrentDirectoryPath(fileManager.temporaryDirectory.path))
39+
let url = URL(fileURLWithPath: "SQLyra.db", isDirectory: false)
3940
defer {
40-
database = nil // closing before remove a file
41+
// closing database before remove a file
4142
#expect(throws: Never.self) { try fileManager.removeItem(at: url) }
4243
}
43-
#expect(!database.isReadonly)
44-
#expect(database.filename == url.path)
45-
#expect(fileManager.fileExists(atPath: url.path))
44+
do {
45+
let database = try Database.open(at: "SQLyra.db", options: [.readwrite, .create])
46+
#expect(!database.isReadonly)
47+
#expect(database.filename == url.path)
48+
#expect(fileManager.fileExists(atPath: url.path))
49+
}
4650
}
4751

48-
@Test func openError() throws {
49-
do {
50-
let database = try Database.open(at: path, options: [])
51-
Issue.record("no error \(database)")
52-
} catch let error { // DatabaseError
53-
#expect(error.code == SQLITE_MISUSE)
52+
@Test func openError() {
53+
#expect(throws: DatabaseError.self) {
54+
try Database.open(at: "db.sqlite", options: [])
5455
}
5556
}
5657

5758
@Test func memory() throws {
58-
let database = try Database.open(at: path, options: [.readwrite, .memory])
59+
let database = try Database.open(at: ":memory:", options: [.readwrite, .memory])
5960
#expect(!database.isReadonly)
6061
#expect(database.filename == "")
6162
}
6263

6364
@Test func readonly() throws {
64-
let database = try Database.open(at: path, options: [.readonly, .memory])
65+
let database = try Database.open(at: ":memory:", options: [.readonly, .memory])
6566
#expect(database.isReadonly)
6667
#expect(database.filename == "")
6768
}
6869

6970
@Test func execute() throws {
70-
let database = try Database.open(at: path, options: [.readwrite, .memory])
71+
let database = try Database.open(at: ":memory:", options: [.readwrite, .memory])
7172

7273
let sql = """
7374
CREATE TABLE contacts(id INT PRIMARY KEY NOT NULL, name TEXT);

0 commit comments

Comments
 (0)