GRDB.swift v2.0 Release Notes

  • ๐Ÿš€ Released September 16, 2017 • diff

    ๐Ÿ‘ GRDB 2.0 brings support for Swift 4. Notable changes are:

    • ๐Ÿ‘‰ Use subscript notation when extracting row values (documentation):

      let name: String = row[0]              // 0 is the leftmost column
      let name: String = row["name"]         // Leftmost matching column - lookup is case-insensitive
      let name: String = row[Column("name")] // Using query interface's Column
      
    • ๐Ÿ‘Œ Support for the Codable protocol (documentation)

      Record types that adopt the standard Codable protocol are granted with automatic adoption of GRDB record protocols. This means that you no longer have to write boilerplate code :tada::

      struct Player : RowConvertible, Persistable, Codable {
          static let databaseTableName = "players"
          let name: String
          let score: Int
      }
      
      // Automatically derived:
      //
      // extension Player {
      //     init(row: Row) {
      //         name = row["name"]
      //         score = row["score"]
      //     }
      //     
      //     func encode(to container: inout PersistenceContainer) {
      //         container["name"] = name
      //         container["score"] = score
      //     }
      // }
      
      try dbQueue.inDatabase { db in
          let arthur = Player(name: "Arthur", score: 100)
          try arthur.insert(db)
          let players = try Player.fetchAll(db) // [Players]
      }
      
    • Records can now specify the columns they feed from (documentation).

      In previous versions of GRDB, SELECT * was the norm. GRDB 2.0 introduces databaseSelection, which allows any type to define its preferred set of columns:

      struct Player : RowConvertible, TableMapping {
          let id: Int64
          let name: String
      
          enum Columns {
              static let id = Column("id")
              static let name = Column("name")
          }
      
          init(row: Row) {
              id = row[Columns.id]
              name = row[Columns.name]
          }
      
          static let databaseTableName = "players"
          static let databaseSelection: [SQLSelectable] = [Columns.id, Columns.name]
      }
      
      // SELECT id, name FROM players
      let players = Player.fetchAll(db)
      
    • Record protocols have more precise semantics: RowConvertible reads database rows, TableMapping builds SQL requests, and Persistable writes (documentation).

      This means that with GRDB 2.0, being able to write Player.fetchAll(db) does not imply that Player.deleteAll(db) is available: you have a better control on the abilities of your record types.

    ๐Ÿ›  Fixed

    • GRDB is now able to store and load zero-length blobs.

    ๐Ÿ†• New

    ๐Ÿ†• New features have been added in order to plug a few holes and support the RxGRDB and GRDBObjc companion projects:

    • Persistable records can export themselves as dictionaries:

      let player = try Player.fetchOne(db, key: 1)
      let dict = player.databaseDictionary // [String: DatabaseValue]
      print(dict)
      // Prints {"id": 1, "name": "Arthur", "score": 1000}
      
    • Query interface requests learned how to limit the number of deleted rows:

      // Delete the last ten players:
      // DELETE FROM players ORDER BY score LIMIT 10
      let request = Player.order(scoreColumn).limit(10)
      try request.deleteAll(db)
      
    • Prepared statements know the index of their columns:

      let statement = try db.makeSelectStatement("SELECT a, b FROM t")
      statement.index(ofColumn: "b")  // 1
      
    • Row cursors (of type RowCursor) expose their underlying statement:

      let rows = try Row.fetchCursor(db, "SELECT ...")
      let statement = rows.statement
      
    • ๐Ÿ— One can build a Set from a cursor:

      let strings = try Set(String.fetchCursor(...))
      
    • The new AnyDatabaseReader and AnyDatabaseWriter type erasers help dealing with the DatabaseReader and DatabaseWriter protocols.

    ๐Ÿ’ฅ Breaking Changes

    • Requirements have changed: Xcode 9+ / Swift 4

    • ๐Ÿ”— WatchOS extension targets no longer need libsqlite3.tbd to be added to the Linked Frameworks and Libraries section of their General tab.

    • The Row.value method has been replaced with subscript notation:

      -row.value(atIndex: 0)
      -row.value(named: "id")
      -row.value(Column("id"))
      +row[0]
      +row["id"]
      +row[Column("id")]
      
    • ๐Ÿ‘ Date and NSDate now interpret numerical database values as timestamps that fuel Date(timeIntervalSince1970:). Previous version of GRDB would interpret numbers as julian days (a date representation supported by SQLite). Support for julian days remains, with the Date(julianDay:) initializer.

    • ๐Ÿšš All TableMapping methods that would modify the database have moved to MutablePersistable, now the only record protocol that is able to write.

    • The TableMapping.selectsRowID property has been replaced with TableMapping.databaseSelection.

      To upgrade, replace:

       struct Player: TableMapping {
      -    static let selectsRowID = true
      +    static let databaseSelection: [SQLSelectable] = [AllColumns(), Column.rowID]
       }
      
    • ๐Ÿšš The DatabaseCursor type has been removed, and replaced with several concrete types that all adopt the Cursor protocol. Those new types allow dedicated optimizations depending on the type of the fetched elements.

      If your application has code that depends on DatabaseCursor, make it target the new concrete types, or make it generic on the Cursor protocol, just like you'd write generic methods on the Swift Sequence protocol.

    • RowConvertible.fetchCursor(_:keys:) returns a non-optional cursor.

    • Database.primaryKey(_:) returns a non-optional PrimaryKeyInfo. When a table has no explicit primary key, the result is the hidden rowid column.

    • ๐Ÿšš The deprecated TableMapping.primaryKeyRowComparator method has been removed.

    ๐Ÿ“š Documentation Diff

    API diff

     class Row {
    -    func value(atIndex index: Int) -> DatabaseValueConvertible?
    -    func value<Value: DatabaseValueConvertible>(atIndex index: Int) -> Value?
    -    func value<Value: DatabaseValueConvertible>(atIndex index: Int) -> Value
    -    func value(named columnName: String) -> DatabaseValueConvertible?
    -    func value<Value: DatabaseValueConvertible>(named columnName: String) -> Value?
    -    func value<Value: DatabaseValueConvertible>(named columnName: String) -> Value
    -    func value(_ column: Column) -> DatabaseValueConvertible?
    -    func value<Value: DatabaseValueConvertible>(_ column: Column) -> Value?
    -    func value<Value: DatabaseValueConvertible>(_ column: Column) -> Value
    +    subscript(_ index: Int) -> DatabaseValueConvertible?
    +    subscript<Value: DatabaseValueConvertible>(_ index: Int) -> Value?
    +    subscript<Value: DatabaseValueConvertible>(_ index: Int) -> Value
    +    subscript(_ columnName: String) -> DatabaseValueConvertible?
    +    subscript<Value: DatabaseValueConvertible>(_ columnName: String) -> Value?
    +    subscript<Value: DatabaseValueConvertible>(_ columnName: String) -> Value
    +    subscript(_ column: Column) -> DatabaseValueConvertible?
    +    subscript<Value: DatabaseValueConvertible>(_ column: Column) -> Value?
    +    subscript<Value: DatabaseValueConvertible>(_ column: Column) -> Value
     }
    
     class SelectStatement {
    +    func index(ofColumn columnName: String) -> Int?
     }
    
    +extension MutablePersistable {
    +    var databaseDictionary: [String: DatabaseValue]
    +}
    
    -extension TableMapping {
    +extension MutablePersistable {
         @discardableResult static func deleteAll(_ db: Database) throws -> Int
         @discardableResult static func deleteAll<Sequence: Swift.Sequence>(_ db: Database, keys: Sequence) throws -> Int where Sequence.Element: DatabaseValueConvertible
         @discardableResult static func deleteOne<PrimaryKeyType: DatabaseValueConvertible>(_ db: Database, key: PrimaryKeyType?) throws -> Bool
         @discardableResult static func deleteAll(_ db: Database, keys: [[String: DatabaseValueConvertible?]]) throws -> Int
         @discardableResult static func deleteOne(_ db: Database, key: [String: DatabaseValueConvertible?]) throws -> Bool
     }
    -extension QueryInterfaceRequest {
    +extension QueryInterfaceRequest where RowDecoder: MutablePersistable {
         @discardableResult func deleteAll(_ db: Database) throws -> Int
     }
    
     protocol TableMapping {
    -    static var selectsRowID: Bool { get }
    +    static var databaseSelection: [SQLSelectable] { get }
     }
     class Record {
    -    open class var selectsRowID: Bool
    +    open class var databaseSelection: [SQLSelectable]
     }
    +struct AllColumns {
    +    init()
    +}
    
     class Database {
    -    func primaryKey(_ tableName: String) throws -> PrimaryKeyInfo?
    +    func primaryKey(_ tableName: String) throws -> PrimaryKeyInfo
     }
     struct PrimaryKeyInfo {
    +    var isRowID: Bool { get }
     }
    
    +extension Set {
    +    init<C: Cursor>(_ cursor: C) throws where C.Element == Element
    +}
    
    -final class DatabaseCursor { }
    +final class ColumnCursor<Value: DatabaseValueConvertible & StatementColumnConvertible> : Cursor {
    +    typealias Element = Value
    +}
    +final class DatabaseValueCursor<Value: DatabaseValueConvertible> : Cursor  {
    +    typealias Element = Value
    +}
    +final class NullableColumnCursor<Value: DatabaseValueConvertible & StatementColumnConvertible> : Cursor  {
    +    typealias Element = Value?
    +}
    +final class NullableDatabaseValueCursor<Value: DatabaseValueConvertible> : Cursor  {
    +    typealias Element = Value?
    +}
    +final class RecordCursor<Record: RowConvertible> : Cursor  {
    +    typealias Element = Record
    +}
    +final class RowCursor : Cursor {
    +    typealias Element = Row
    +    let statement: SelectStatement
    +}
     extension DatabaseValueConvertible {
    -    static func fetchCursor(...) throws -> DatabaseCursor<Self>
    +    static func fetchCursor(...) throws -> DatabaseValueCursor<Self>
     }
     extension DatabaseValueConvertible where Self: StatementColumnConvertible {
    +    static func fetchCursor(...) throws -> ColumnCursor<Self>
     }
     extension Optional where Wrapped: DatabaseValueConvertible {
    -    static func fetchCursor(...) throws -> DatabaseCursor<Wrapped?>
    +    static func fetchCursor(...) throws -> NullableDatabaseValueCursor<Wrapped>
     }
     extension Optional where Wrapped: DatabaseValueConvertible & StatementColumnConvertible {
    +    static func fetchCursor(...) throws -> NullableColumnCursor<Wrapped>
     }
     extension Row {
    -    static func fetchCursor(...) throws -> DatabaseCursor<Row>
    +    static func fetchCursor(...) throws -> RowCursor
     }
     extension RowConvertible where Self: TableMapping {
    -    static func fetchCursor(...) throws -> DatabaseCursor<Self>?
    +    static func fetchCursor(...) throws -> RecordCursor<Self>
     }
     extension TypedRequest where RowDecoder: RowConvertible {
    -    func fetchCursor(_ db: Database) throws -> DatabaseCursor<RowDecoder>
    +    func fetchCursor(_ db: Database) throws -> RecordCursor<RowDecoder>
     }
     extension TypedRequest where RowDecoder: DatabaseValueConvertible {
    -    func fetchCursor(_ db: Database) throws -> DatabaseCursor<RowDecoder>
    +    func fetchCursor(_ db: Database) throws -> DatabaseValueCursor<RowDecoder>
     }
     extension TypedRequest where RowDecoder: DatabaseValueConvertible & StatementColumnConvertible {
    +    func fetchCursor(_ db: Database) throws -> ColumnCursor<RowDecoder>
     }
     extension TypedRequest where RowDecoder: _OptionalProtocol, RowDecoder._Wrapped: DatabaseValueConvertible {
    -    func fetchCursor(_ db: Database) throws -> DatabaseCursor<RowDecoder._Wrapped?>
    +    func fetchCursor(_ db: Database) throws -> NullableDatabaseValueCursor<RowDecoder._Wrapped>
     }
     extension TypedRequest where RowDecoder: _OptionalProtocol, RowDecoder._Wrapped: DatabaseValueConvertible & StatementColumnConvertible {
    +    func fetchCursor(_ db: Database) throws -> NullableColumnCursor<RowDecoder._Wrapped>
     }
     extension TypedRequest where RowDecoder: Row {
    -    func fetchCursor(_ db: Database) throws -> DatabaseCursor<Row>
    +    func fetchCursor(_ db: Database) throws -> RowCursor
     }
    
     extension TableMapping {
    -    @available(*, deprecated) static func primaryKeyRowComparator(_ db: Database) throws -> (Row, Row) -> Bool
     }
    
    +final class AnyDatabaseReader : DatabaseReader {
    +     init(_ base: DatabaseReader)
    +}
    +final class AnyDatabaseWriter : DatabaseWriter {
    +     init(_ base: DatabaseWriter)
    +}
    

    Experimental API diff

     enum SQLCount {
    -    case star
    +    case all
     }