Skip to content

Commit 97f5c94

Browse files
committed
Some tests
1 parent 52e4e1f commit 97f5c94

10 files changed

Lines changed: 129 additions & 97 deletions

Sources/Feather/ConnectionPool.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public actor ConnectionPool: Sendable {
5959
}
6060

6161
/// Whether or not we have created all the connections we are allowed too
62-
private var isAtConnectionLimit: Bool {
62+
var isAtConnectionLimit: Bool {
6363
return count >= limit
6464
}
6565

@@ -89,7 +89,7 @@ public actor ConnectionPool: Sendable {
8989
private func getConnection() async throws(FeatherError) -> SQLiteConnection {
9090
guard availableConnections.isEmpty else {
9191
// Have an available connection, just use it
92-
return availableConnections.removeFirst()
92+
return availableConnections.removeLast()
9393
}
9494

9595
guard !isAtConnectionLimit else {

Sources/Feather/Cursor.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import SQLite3
99

1010
public struct Cursor<Element: RowDecodable>: ~Copyable {
1111
private let statement: Statement
12-
private var column: Int32 = 0
1312

1413
public init(of statement: consuming Statement) {
1514
self.statement = statement

Sources/Feather/Database.swift

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,35 @@
55
// Created by Wes Wickwire on 5/4/25.
66
//
77

8+
import Foundation
9+
10+
/// The base protocol every generated database conforms too.
811
public protocol Database {
12+
/// The connection to use
913
init(connection: any Connection)
14+
/// An ordered list of migrations to be run.
1015
static var migrations: [String] { get }
1116
}
1217

1318
public extension Database {
19+
/// Opens a connection pool to the database at the given URL.
20+
///
21+
/// - Parameter url: The url of the database file
22+
init(url: URL) throws {
23+
self = try Self(path: url.path)
24+
}
25+
26+
/// Opens a connection pool to the database at the given path.
27+
///
28+
/// - Parameter path: The path of the database file
29+
init(path: String) throws {
30+
self = try Self(config: DatabaseConfig(path: path))
31+
}
32+
33+
/// Opens a connection pool to the database
34+
///
35+
/// - Parameter config: The configuration specifying any info
36+
/// needed to open the database.
1437
init(config: DatabaseConfig) throws {
1538
let connection: any Connection = if let path = config.path {
1639
try ConnectionPool(
@@ -29,6 +52,8 @@ public extension Database {
2952
self = Self(connection: connection)
3053
}
3154

55+
56+
/// Creates an in memory database.
3257
static func inMemory() throws -> Self {
3358
return try Self(config: DatabaseConfig(path: nil))
3459
}

Sources/Feather/DatabasePrimitive.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ import Foundation
1010

1111
@usableFromInline let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self)
1212

13+
/// A type that is mapped directly to a SQLite type.
14+
///
15+
/// You **should not** be conforming any of your types to this directly.
16+
/// It will have no effect. For custom type conversion see `DatabasePrimitiveConvertible`
1317
public protocol DatabasePrimitive: RowDecodable {
1418
init(from cursor: OpaquePointer, at index: Int32) throws(FeatherError)
1519
func bind(to statement: OpaquePointer, at index: Int32) throws(FeatherError)

Sources/Feather/DatabaseQuery.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
// Created by Wes Wickwire on 11/9/24.
66
//
77

8+
/// A query which reads or writes to a database.
89
public protocol DatabaseQuery<Input, Output>: Query {
910
/// Whether the query requires a read or write transaction.
1011
var transactionKind: Transaction.Kind { get }

Sources/Feather/FeatherError.swift

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
// Created by Wes Wickwire on 2/16/25.
66
//
77

8-
public enum FeatherError: Error {
8+
public enum FeatherError: Error, Equatable {
99
case failedToOpenConnection(path: String)
1010
case failedToInitializeStatement
1111
case columnIsNil(Int32)
@@ -21,10 +21,19 @@ public enum FeatherError: Error {
2121
/// to be started twice.
2222
case subscriptionAlreadyStarted
2323
case invalidUuidString
24-
case cannotDecode(Any.Type, from: Any.Type)
25-
case cannotEncode(Any.Type, to: Any.Type)
24+
case cannotDecode(String, from: String)
25+
case cannotEncode(String, to: String)
2626
case decodingError(String)
2727
case encodingError(String)
2828
case requiredAssociationFailed(parent: String, childKey: String)
2929
case cannotObserveWriteQuery
30+
case cannotWriteInAReadTransaction
31+
32+
public static func cannotDecode(_ type: Any.Type, from otherType: Any.Type) -> FeatherError {
33+
return .cannotDecode("\(type)", from: "\(otherType)")
34+
}
35+
36+
public static func cannotEncode(_ type: Any.Type, to otherType: Any.Type) -> FeatherError {
37+
return .cannotEncode("\(type)", to: "\(otherType)")
38+
}
3039
}

Sources/Feather/Queries/AnyDatabaseQuery.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,11 @@ public struct AnyDatabaseQuery<Input, Output>: DatabaseQuery
4444
with input: Input,
4545
tx: borrowing Transaction
4646
) throws -> Output {
47-
try execute(input, tx)
47+
guard tx.kind >= transactionKind else {
48+
throw FeatherError.cannotWriteInAReadTransaction
49+
}
50+
51+
return try execute(input, tx)
4852
}
4953
}
5054

Tests/FeatherTests/ConnectionPoolTests.swift

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
// Created by Wes Wickwire on 11/9/24.
66
//
77

8+
import Foundation
89
import Testing
910
@testable import Feather
1011

@@ -18,26 +19,23 @@ struct ConnectionPoolTests {
1819
let pool = try ConnectionPool(
1920
path: ":memory:",
2021
limit: 1,
21-
migrations: [
22-
"CREATE TABLE foo (bar INTEGER)"
23-
]
22+
migrations: ["CREATE TABLE foo (bar INTEGER)"]
2423
)
2524

2625
try await pool.begin(.read) { tx in
2726
_ = try Statement("SELECT * FROM foo", transaction: tx)
2827
}
2928
}
3029

31-
@Test func poolReusesConnections() async throws {
30+
@Test func poolReclaimsConnectionWhenFinished() async throws {
3231
let pool = try ConnectionPool(
3332
path: ":memory:",
3433
limit: 1,
35-
migrations: [
36-
"CREATE TABLE foo (bar INTEGER)"
37-
]
34+
migrations: ["CREATE TABLE foo (bar INTEGER)"]
3835
)
3936

4037
// Will hang indefinitely if a connection isnt recycled
38+
// since its a single connection
4139
for _ in 0..<10 {
4240
try await pool.begin(.read) { tx in
4341
_ = try Statement("SELECT * FROM foo", transaction: tx)

Tests/FeatherTests/QueryTests.swift

Lines changed: 28 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -11,99 +11,44 @@ import Testing
1111

1212
@Suite
1313
struct QueryTests {
14-
@Test func testQuery() async throws {
15-
let pool = try createDatabase()
16-
let insert = insertQuery(database: pool)
14+
@Test func testInsertAndGetQuery() async throws {
15+
let db = try TestDB.inTempDir()
16+
try await db.insertFoo.execute(with: 1)
1717

18-
let foo1 = Foo(bar: 1, baz: "bar1")
19-
let foo2 = Foo(bar: 2, baz: "bar2")
20-
let foo3 = Foo(bar: 3, baz: "bar3")
18+
let foos = try await db.selectFoos.execute()
19+
#expect(foos == [TestDB.Foo(bar: 1)])
2120

22-
try await insert.execute(with: foo1)
23-
try await insert.execute(with: foo2)
24-
try await insert.execute(with: foo3)
25-
26-
let foos = try await selectAllFooQuery(database: pool).execute()
27-
28-
#expect(foos.count == 3)
21+
let foo = try await db.selectFoo.execute(with: 1)
22+
#expect(foo == TestDB.Foo(bar: 1))
2923
}
3024

31-
// @Test func testMacroQuery() async throws {
32-
// let pool = try ConnectionPool(path: ":memory:", limit: 1, migrations: TestDB.migrations)
33-
//
34-
// try await TestDB.insertFoo.execute(with: .init(bar: 1, baz: "one"), in: pool)
35-
// try await TestDB.insertFoo.execute(with: .init(bar: 2, baz: "two"), in: pool)
36-
//
37-
// let foos = try await TestDB.fetchFoos.execute(in: pool)
38-
// #expect(foos.count == 2)
39-
// }
25+
@Test func selectManyWithEmptyDbReturnsEmpty() async throws {
26+
let db = try TestDB.inTempDir()
27+
let foos = try await db.selectFoos.execute()
28+
#expect(foos == [])
29+
}
4030

41-
struct Foo: RowDecodable {
42-
let bar: Int
43-
let baz: String?
44-
45-
init(bar: Int, baz: String?) {
46-
self.bar = bar
47-
self.baz = baz
48-
}
49-
50-
init(row: borrowing Row, startingAt start: Int32) throws(FeatherError) {
51-
self.bar = try row.value(at: 0)
52-
self.baz = try row.value(at: 1)
53-
}
31+
@Test func selectManyCanReturnManyItems() async throws {
32+
let db = try TestDB.inTempDir()
33+
try await db.insertFoo.execute(with: 1)
34+
try await db.insertFoo.execute(with: 2)
35+
let foos = try await db.selectFoos.execute()
36+
#expect(foos == [TestDB.Foo(bar: 1), TestDB.Foo(bar: 2)])
5437
}
5538

56-
private func selectAllFooQuery(database: any Connection) -> any DatabaseQuery<(), [Foo]> {
57-
return AnyDatabaseQuery<(), [Foo]>(.read, in: database, watchingTables: []) { input, transaction in
58-
let statement = try Statement(in: transaction) {
59-
"SELECT * FROM foo;"
60-
}
61-
62-
return try statement.fetchAll(of: Foo.self)
63-
}
39+
@Test func selectSingleWithEmptyDbReturnsNil() async throws {
40+
let db = try TestDB.inTempDir()
41+
let foo = try await db.selectFoo.execute(with: 1)
42+
#expect(foo == nil)
6443
}
6544

66-
private func insertQuery(database: any Connection) -> any DatabaseQuery<Foo, ()> {
67-
return AnyDatabaseQuery<Foo, ()>(.write, in: database, watchingTables: []) { input, transaction in
68-
let statement = try Statement(in: transaction) {
69-
"INSERT INTO foo (bar, baz) VALUES (?, ?)"
70-
} bind: { statement in
71-
try statement.bind(value: input.bar, to: 1)
72-
try statement.bind(value: input.baz, to: 2)
45+
@Test func errorIsThrownWhenAttemptingToWriteToReadTx() async throws {
46+
let db = try TestDB.inTempDir()
47+
48+
await #expect(throws: FeatherError.cannotWriteInAReadTransaction) {
49+
_ = try await db.connection.begin(.read) { tx in
50+
try db.insertFoo.execute(with: 1, tx: tx)
7351
}
74-
75-
_ = try statement.step()
7652
}
7753
}
78-
79-
private func createDatabase() throws -> ConnectionPool {
80-
return try ConnectionPool(
81-
path: ":memory:",
82-
limit: 1,
83-
migrations: [
84-
"CREATE TABLE foo (bar INTEGER PRIMARY KEY, baz TEXT)"
85-
]
86-
)
87-
}
88-
//
89-
// @Database
90-
// struct TestDB: Database {
91-
// static var migrations: [String] {
92-
// return [
93-
// "CREATE TABLE foo (bar INTEGER PRIMARY KEY, baz TEXT)"
94-
// ]
95-
// }
96-
//
97-
// static var queries: [String] {
98-
// return [
99-
// """
100-
// DEFINE QUERY fetchFoos AS
101-
// SELECT * FROM foo;
102-
//
103-
// DEFINE QUERY insertFoo AS
104-
// INSERT INTO foo (bar, baz) VALUES (?, ?);
105-
// """,
106-
// ]
107-
// }
108-
// }
10954
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
//
2+
// TestDatabase.swift
3+
// Feather
4+
//
5+
// Created by Wes Wickwire on 6/14/25.
6+
//
7+
8+
import Feather
9+
import Foundation
10+
11+
/// A database to use for unit tests
12+
@Database
13+
struct TestDB {
14+
@Query("SELECT * FROM foo")
15+
var selectFoos: SelectFoosDatabaseQuery
16+
17+
@Query("SELECT * FROM foo WHERE bar = ?")
18+
var selectFoo: SelectFooDatabaseQuery
19+
20+
@Query("INSERT INTO foo (bar) VALUES (?)")
21+
var insertFoo: InsertFooDatabaseQuery
22+
23+
static var migrations: [String] {
24+
return [
25+
"CREATE TABLE foo (bar INTEGER);"
26+
]
27+
}
28+
}
29+
30+
extension TestDB {
31+
/// Creates a database on disk in the temp directory.
32+
/// Will delete the DB if it already exists
33+
static func inTempDir(
34+
name: StaticString = #function,
35+
maxConnectionCount: Int = 5
36+
) throws -> TestDB {
37+
let temp = FileManager.default.temporaryDirectory
38+
.appending(component: "\(name).db")
39+
40+
if FileManager.default.fileExists(atPath: temp.path) {
41+
try FileManager.default.removeItem(atPath: temp.path)
42+
}
43+
44+
let config = DatabaseConfig(path: temp.path, maxConnectionCount: maxConnectionCount)
45+
return try TestDB(config: config)
46+
}
47+
}

0 commit comments

Comments
 (0)