GRDB.swift: DatabaseValueConvertible use throws methods instead of properties

Currently, since DatabaseValueConvertible uses a property or a non throwing method, any errors in the conversion process cannot be propagated and there is no way to cancel a current transaction.

Unless I am missing something, the only options available to the implementor is to log an error and return some valid value (e.g. database null) or use some horrible overreach such as fatalError.

Switching this protocol to use throwing methods for both the to & from direction would allow errors during this phase to propagate and successfully fail any in-flight transactions.

A proposed implementation would be:

public protocol DatabaseValueConvertible : SQLExpressible {
    /// Returns a value that can be stored in the database.
    func databaseValue() throws -> DatabaseValue
    
    /// Returns a value initialized from *databaseValue*, if possible.
    static func fromDatabaseValue(_ databaseValue: DatabaseValue) throws -> Self?
}

If you are open to this change, I’d be more than happy to submit a PR implement it as such.

About this issue

  • Original URL
  • State: closed
  • Created 8 years ago
  • Comments: 15 (15 by maintainers)

Commits related to this issue

Most upvoted comments

@kdubb Your questions did ring a bell in me. Although I still stand by non-failable DatabaseValueConvertible and RowConvertible conversion protocols, I have to admit that the state of handling errors that happen when reading from the database lacks a lot:

// A chore
let request = Message.all()
let (statement, adapter) = try request.prepare(db)
let rows = Row.fetch(statement, adapter: adapter)
let iterator = rows.makeIterator()
while let row = try iterator.step() {
    let message = try Message(row: row)
}

That’s why I’ve pushed to the DatabaseCursor branch support for a new type: DatabaseCursor.

Unlike DatabaseSequence which can not throw (because of Swift), DatabaseCursor throws whenever it can.

The code above can now read:

let request = Message.all()
let cursor = try Row.fetchCursor(db, request)
while let row = try cursor.step() {
    let message = try Message(row: row)
}

Of course, with records that can always initialize from rows, it’s even shorter:

let cursor = try Message.fetchCursor(db)
while let message = try cursor.step() {
    ...
}

The fetchCursor() methods completes the list of fetching methods:

try Type.fetchCursor(...) // DatabaseCursor<Type>
Type.fetch(...)           // DatabaseSequence<Type>
Type.fetchAll(...)        // [Type]
Type.fetchOne(...)        // Type?

I think this branch may help you.