Skip to content

Commit

Permalink
Tidy code, async-ify the last non-async test, add custom JSON coders …
Browse files Browse the repository at this point in the history
…test
  • Loading branch information
gwynne committed May 14, 2024
1 parent 05536ab commit ddb24f5
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 58 deletions.
5 changes: 3 additions & 2 deletions Sources/FluentSQLiteDriver/SQLiteConverterDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import SQLKit
import FluentKit

struct SQLiteConverterDelegate: SQLConverterDelegate {
func customDataType(_ dataType: DatabaseSchema.DataType) -> SQLExpression? {
func customDataType(_ dataType: DatabaseSchema.DataType) -> (any SQLExpression)? {
switch dataType {
case .string:
return SQLRaw("TEXT")
Expand All @@ -13,7 +13,8 @@ struct SQLiteConverterDelegate: SQLConverterDelegate {
return SQLRaw("INTEGER")
case .enum:
return SQLRaw("TEXT")
default: return nil
default:
return nil
}
}
}
49 changes: 28 additions & 21 deletions Sources/FluentSQLiteDriver/SQLiteRow+Database.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,43 +3,50 @@ import SQLiteNIO
import SQLiteKit
import FluentKit

extension SQLiteRow {
extension SQLRow {
/// Returns a `DatabaseOutput` for this row.
///
/// - Parameter decoder: An `SQLiteDataDecoder` used to translate `SQLiteData` values into output values.
///
/// - Returns: A `DatabaseOutput` instance.
func databaseOutput(decoder: SQLiteDataDecoder) -> any DatabaseOutput {
SQLiteDatabaseOutput(row: self.sql(decoder: decoder), schema: nil)
func databaseOutput() -> some DatabaseOutput {
SQLRowDatabaseOutput(row: self, schema: nil)
}
}

/// A `DatabaseOutput` implementation for `SQLiteRow`s.
private struct SQLiteDatabaseOutput: DatabaseOutput {
/// A `DatabaseOutput` implementation for generic `SQLRow`s. This should really be in FluentSQL.
private struct SQLRowDatabaseOutput: DatabaseOutput {
/// The underlying row.
let row: any SQLRow

/// The most recently set schema value (see `DatabaseOutput.schema(_:)`).
let schema: String?

private func column(for key: FieldKey) -> String {
(self.schema.map { FieldKey.prefix(.prefix(.string($0), "_"), key) } ?? key).description
// See `CustomStringConvertible.description`.
var description: String {
String(describing: self.row)
}

func schema(_ schema: String) -> DatabaseOutput {
SQLiteDatabaseOutput(row: self.row, schema: schema)
/// Apply the current schema (if any) to the given `FieldKey` and convert to a column name.
private func adjust(key: FieldKey) -> String {
(self.schema.map { .prefix(.prefix(.string($0), "_"), key) } ?? key).description
}


// See `DatabaseOutput.schema(_:)`.
func schema(_ schema: String) -> any DatabaseOutput {
SQLRowDatabaseOutput(row: self.row, schema: schema)
}

// See `DatabaseOutput.contains(_:)`.
func contains(_ key: FieldKey) -> Bool {
self.row.contains(column: self.column(for: key))
self.row.contains(column: self.adjust(key: key))
}


// See `DatabaseOutput.decodeNil(_:)`.
func decodeNil(_ key: FieldKey) throws -> Bool {
try self.row.decodeNil(column: self.column(for: key))
}

func decode<T: Decodable>(_ key: FieldKey, as type: T.Type) throws -> T {
try self.row.decode(column: self.column(for: key), as: T.self)
try self.row.decodeNil(column: self.adjust(key: key))
}

var description: String { "" }
// See `DatabaseOutput.decode(_:as:)`.
func decode<T: Decodable>(_ key: FieldKey, as: T.Type) throws -> T {
try self.row.decode(column: self.adjust(key: key), as: T.self)
}
}
124 changes: 89 additions & 35 deletions Tests/FluentSQLiteDriverTests/FluentSQLiteDriverTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,23 @@ func XCTAssertThrowsErrorAsync<T>(
}
}

func XCTAssertNoThrowAsync<T>(
_ expression: @autoclosure () async throws -> T,
_ message: @autoclosure () -> String = "",
file: StaticString = #filePath, line: UInt = #line
) async {
do {
_ = try await expression()
} catch {
XCTAssertNoThrow(try { throw error }(), message(), file: file, line: line)
}
}

final class FluentSQLiteDriverTests: XCTestCase {
func testAggregate() throws { try self.benchmarker.testAggregate() }
func testArray() throws { try self.benchmarker.testArray() }
func testBatch() throws { try self.benchmarker.testBatch() }
func testChild() throws { try self.benchmarker.testChild() }
func testChildren() throws { try self.benchmarker.testChildren() }
func testCodable() throws { try self.benchmarker.testCodable() }
func testChunk() throws { try self.benchmarker.testChunk() }
Expand Down Expand Up @@ -58,11 +70,13 @@ final class FluentSQLiteDriverTests: XCTestCase {

func testDatabaseError() async throws {
let sql = (self.database as! any SQLDatabase)

await XCTAssertThrowsErrorAsync(try await sql.raw("asdf").run()) {
XCTAssertTrue(($0 as? any DatabaseError)?.isSyntaxError ?? false, "\(String(reflecting: $0))")
XCTAssertFalse(($0 as? any DatabaseError)?.isConstraintFailure ?? true, "\(String(reflecting: $0))")
XCTAssertFalse(($0 as? any DatabaseError)?.isConnectionClosed ?? true, "\(String(reflecting: $0))")
}

try await sql.drop(table: "foo").ifExists().run()
try await sql.create(table: "foo").column("name", type: .text, .unique).run()
try await sql.insert(into: "foo").columns("name").values("bar").run()
Expand All @@ -74,77 +88,121 @@ final class FluentSQLiteDriverTests: XCTestCase {
}

// https://github.com/vapor/fluent-sqlite-driver/issues/62
func testUnsupportedUpdateMigration() throws {
struct UserMigration_v1_0_0: Migration {
func prepare(on database: Database) -> EventLoopFuture<Void> {
database.schema("users")
func testUnsupportedUpdateMigration() async throws {
struct UserMigration_v1_0_0: AsyncMigration {
func prepare(on database: any Database) async throws {
try await database.schema("users")
.id()
.field("email", .string, .required)
.field("password", .string, .required)
.unique(on: "email")
.create()
}

func revert(on database: Database) -> EventLoopFuture<Void> {
database.schema("users").delete()
func revert(on database: any Database) async throws {
try await database.schema("users").delete()
}
}
struct UserMigration_v1_2_0: Migration {
func prepare(on database: Database) -> EventLoopFuture<Void> {
database.schema("users")

struct UserMigration_v1_2_0: AsyncMigration {
func prepare(on database: any Database) async throws {
try await database.schema("users")
.field("apple_id", .string)
.unique(on: "apple_id")
.update()
}

func revert(on database: Database) -> EventLoopFuture<Void> {
database.schema("users")
func revert(on database: any Database) async throws {
try await database.schema("users")
.deleteField("apple_id")
.update()
}
}
try UserMigration_v1_0_0().prepare(on: self.database).wait()

try await UserMigration_v1_0_0().prepare(on: self.database)
await XCTAssertThrowsErrorAsync(try await UserMigration_v1_2_0().prepare(on: self.database)) {
XCTAssert(String(describing: $0).contains("adding columns"))
}
await XCTAssertThrowsErrorAsync(try await UserMigration_v1_2_0().revert(on: self.database)) {
XCTAssert(String(describing: $0).contains("adding columns"))
}
await XCTAssertNoThrowAsync(try await UserMigration_v1_0_0().revert(on: self.database))
}

func testCustomJSON() async throws {
struct Metadata: Codable { let createdAt: Date }
final class Event: Model, @unchecked Sendable {
static let schema = "events"
@ID(custom: "id", generatedBy: .database) var id: Int?
@Field(key: "metadata") var metadata: Metadata
}
final class EventStringlyTyped: Model, @unchecked Sendable {
static let schema = "events"
@ID(custom: "id", generatedBy: .database) var id: Int?
@Field(key: "metadata") var metadata: [String: String]
}
struct EventMigration: AsyncMigration {
func prepare(on database: any Database) async throws {
try await database.schema(Event.schema)
.field("id", .int, .identifier(auto: false))
.field("metadata", .json, .required)
.create()
}
func revert(on database: any Database) async throws {
try await database.schema(Event.schema).delete()
}
}

let jsonEncoder = JSONEncoder(); jsonEncoder.dateEncodingStrategy = .iso8601
let jsonDecoder = JSONDecoder(); jsonDecoder.dateDecodingStrategy = .iso8601
let iso8601 = DatabaseID(string: "iso8601")

self.dbs.use(.sqlite(.memory, dataEncoder: .init(json: jsonEncoder), dataDecoder: .init(json: jsonDecoder)), as: iso8601)
let db = self.dbs.database(iso8601, logger: .init(label: "test"), on: self.dbs.eventLoopGroup.any())!

try await EventMigration().prepare(on: db)
do {
try UserMigration_v1_2_0().prepare(on: self.database).wait()
try UserMigration_v1_2_0().revert(on: self.database).wait()
let date = Date()
let event = Event()
event.id = 1
event.metadata = Metadata(createdAt: date)
try await event.save(on: db)

let rows = try await EventStringlyTyped.query(on: db).filter(\.$id == 1).all()
XCTAssertEqual(rows[0].metadata["createdAt"], ISO8601DateFormatter().string(from: date))
} catch {
print(error)
XCTAssertTrue("\(error)".contains("adding columns"))
try? await EventMigration().revert(on: db)
throw error
}
try UserMigration_v1_0_0().revert(on: self.database).wait()
}

var benchmarker: FluentBenchmarker {
.init(databases: self.dbs)
try await EventMigration().revert(on: db)
}

var benchmarker: FluentBenchmarker { .init(databases: self.dbs) }
var database: (any Database)!
var dbs: Databases!

let benchmarkPath = FileManager.default.temporaryDirectory.appendingPathComponent("benchmark.sqlite").absoluteString

override class func setUp() {
XCTAssert(isLoggingConfigured)
}

override func setUpWithError() throws {
try super.setUpWithError()

XCTAssert(isLoggingConfigured)
self.dbs = Databases(threadPool: NIOThreadPool.singleton, on: MultiThreadedEventLoopGroup.singleton)
self.dbs.use(.sqlite(.memory), as: .sqlite)
self.dbs.use(.sqlite(.file(self.benchmarkPath)), as: .benchmark)

let a = self.dbs.database(.sqlite, logger: .init(label: "test.fluent.a"), on: MultiThreadedEventLoopGroup.singleton.any())

self.database = a
self.dbs.use(.sqlite(.file(self.benchmarkPath)), as: .init(string: "benchmark"))
self.database = self.dbs.database(.sqlite, logger: .init(label: "test.fluent.sqlite"), on: MultiThreadedEventLoopGroup.singleton.any())
}

override func tearDownWithError() throws {
self.dbs.shutdown()
self.dbs = nil

try super.tearDownWithError()
}
}

func env(_ name: String) -> String? {
return ProcessInfo.processInfo.environment[name]
ProcessInfo.processInfo.environment[name]
}

let isLoggingConfigured: Bool = {
Expand All @@ -155,7 +213,3 @@ let isLoggingConfigured: Bool = {
}
return true
}()

extension DatabaseID {
static let benchmark = DatabaseID(string: "benchmark")
}

0 comments on commit ddb24f5

Please sign in to comment.