Changelog History
Page 12
-
v2.0 Changes
๐ 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 introducesdatabaseSelection
, 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 thatPlayer.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
andAnyDatabaseWriter
type erasers help dealing with theDatabaseReader
andDatabaseWriter
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 theDate(julianDay:)
initializer.๐ All
TableMapping
methods that would modify the database have moved toMutablePersistable
, now the only record protocol that is able to write.The
TableMapping.selectsRowID
property has been replaced withTableMapping.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 theCursor
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 theCursor
protocol, just like you'd write generic methods on the SwiftSequence
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
- โก๏ธ WatchOS installation procedure has been updated with new instructions.
- The introduction to multithreading with database pools has been enhanced.
- The Rows as Dictionaries chapter has been enhanced with instructions for converting a row into a dictionary.
- ๐ The description of cursors better explains how to choose between a cursor and an array.
- โก๏ธ The Date and DateComponents chapter has been updated for the new support for unix timestamps.
- The Fatal Errors chapter has an enhanced description of value conversion errors and how to handle them.
- โก๏ธ The Usage of raw SQLite pointers chapter has been updated for the new
SQLite3
standard module that comes with Swift4. - A new Record Protocols Overview chapter has been added.
- A new Codable Records chapter has been added.
- A new Columns Selected by a Request chapter describes the new
TableMapping.databaseSelection
property. - โก๏ธ The Exposing the RowID Column chapter has been updated for the new
TableMapping.databaseSelection
property.
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 }
-
v1.3.0 Changes
๐ Released August 18, 2017 • diff
๐ New
- โฌ๏ธ Upgrade custom SQLite builds to v3.20.0 (thanks to @swiftlyfalling).
- ๐ Complete support for all signed and unsigned integer types: from Int8 toUInt64, all integers can be stored and loaded from the database.
API diff
+extension Int8: DatabaseValueConvertible, StatementColumnConvertible { } +extension Int16: DatabaseValueConvertible, StatementColumnConvertible { } +extension UInt8: DatabaseValueConvertible, StatementColumnConvertible { } +extension UInt16: DatabaseValueConvertible, StatementColumnConvertible { } +extension UInt32: DatabaseValueConvertible, StatementColumnConvertible { } +extension UInt64: DatabaseValueConvertible, StatementColumnConvertible { } +extension UInt: DatabaseValueConvertible, StatementColumnConvertible { }
-
v1.2.2 Changes
๐ Released July 20, 2017 • diff
๐ Fixed
- ๐ง The
Configuration.trace
function no longer leaks memory.
- ๐ง The
-
v1.2.1 Changes
๐ Released July 19, 2017 • diff
๐ Fixed
- โฌ๏ธ Upgrade custom SQLite builds to v3.19.3 (thanks to @swiftlyfalling).
The Query Interface now generates
IS NULL
SQL snippets for comparisons withDatabaseValue.null
:// SELECT * FROM players WHERE email IS NULL Player.filter(Column("email") == DatabaseValue.null)
It used to generate
= NULL
which would not behave as expected.
-
v1.2.0 Changes
๐ Released July 13, 2017 • diff
๐ New
๐ Record types that do not provide values for all table columns in their
encode(to:)
method are now supported.The table creation API has been enhanced:
SQLite supports untyped columns:
try dbQueue.inDatabase { db in // CREATE TABLE t(a, b) try db.create(table: "t") { t in t.column("a") t.column("b") } }
Untyped columns behave like decimal, boolean, date columns, and generally all columns with NUMERIC type affinity.
This feature addresses #169.
The
indexed()
methods lets you create a non-unique index on a table column:try dbQueue.inDatabase { db in // CREATE TABLE rounds(score INTEGER) // CREATE INDEX rounds_on_score ON rounds(score) try db.create(table: "rounds") { t in t.column("score", .integer).indexed() } }
It is now possible to define references to tables without any explicit primary key. The generated SQL then uses the
rowid
hidden primary key column:try dbQueue.inDatabase { db in // CREATE TABLE nodes( // name TEXT, // parentId INTEGER REFERENCES nodes(rowid) // ) try db.create(table: "nodes") { t in t.column("name", .text) t.column("parentId", .integer).references("nodes") } }
๐
DatabaseQueue
,DatabasePool
and their common protocolDatabaseReader
can now perform "unsafe reentrant reads" (documentation):try dbPool.read { db in // This is allowed try dbPool.unsafeReentrantRead { db in ... } }
๐ Fixed
DatabasePool.read
, andDatabasePool.unsafeRead
now raise a fatal error when used in a reentrant way:try dbPool.read { db in // fatal error: "Database methods are not reentrant." try dbPool.read { db in ... } }
While this change may appear as a breaking change, it is really a fix: reentrant reads deadlock as soon as the maximum number of readers has been reached.
API diff
final class ColumnDefinition { + @discardableResult func indexed() -> Self } final class TableDefinition { - func column(_ name: String, _ type: Database.ColumnType) -> ColumnDefinition + func column(_ name: String, _ type: Database.ColumnType? = nil) -> ColumnDefinition } protocol DatabaseReader { + func unsafeReentrantRead<T>(_ block: (Database) throws -> T) throws -> T }
-
v1.1.0 Changes
๐ Released July 1, 2017 • diff
๐ New
๐
DatabaseAggregate
is the protocol for custom aggregate functions (fixes #236, documentation):struct MySum : DatabaseAggregate { var sum: Int = 0 mutating func step(_ dbValues: [DatabaseValue]) { if let int = Int.fromDatabaseValue(dbValues[0]) { sum += int } } func finalize() -> DatabaseValueConvertible? { return sum } } let dbQueue = DatabaseQueue() let fn = DatabaseFunction("mysum", argumentCount: 1, aggregate: MySum.self) dbQueue.add(function: fn) try dbQueue.inDatabase { db in try db.execute("CREATE TABLE test(i)") try db.execute("INSERT INTO test(i) VALUES (1)") try db.execute("INSERT INTO test(i) VALUES (2)") try Int.fetchOne(db, "SELECT mysum(i) FROM test")! // 3 }
๐ Fixed
- ๐
QueryInterfaceRequest.order(_:)
clears the eventual reversed flag, and better reflects the documentation of this method: "Any previous ordering is replaced."
๐ Deprecated
- ๐
TableMapping.primaryKeyRowComparator
is deprecated, without any replacement.
API diff
final class DatabaseFunction { + init<Aggregate: DatabaseAggregate>(_ name: String, argumentCount: Int32? = nil, pure: Bool = false, aggregate: Aggregate.Type) } +protocol DatabaseAggregate { + init() + mutating func step(_ dbValues: [DatabaseValue]) throws + func finalize() throws -> DatabaseValueConvertible? +} extension TableMapping { + @available(*, deprecated) static func primaryKeyRowComparator(_ db: Database) throws -> (Row, Row) -> Bool }
-
v1.0.0 Changes
๐ Released June 20, 2017 :tada:
GRDB 1.0 comes with enhancements, and API stability.
It comes with breaking changes, but the good news is that they are the last (until GRDB 2.0) :sweat_smile:!
Requirements have changed: Xcode 8.3+ / Swift 3.1
As a matter of fact, GRDB 1.0 still supports Xcode 8.1 and Swift 3.0. But future versions are free to use Swift 3.1 features, and will require Xcode 8.3+.
The targetted operating systems are unchanged: iOS 8.0+ / OSX 10.9+ / watchOS 2.0+
Record types have their
persistentDictionary
property replaced with theencode(to:)
method:struct Player : Persistable { let name: String let score: Int // Old // var persistentDictionary: [String: DatabaseValueConvertible?] { // return [ // "name": name, // "score": score, // ] // } // New func encode(to container: inout PersistenceContainer) { container["name"] = name container["score"] = score } }
This is good for applications that declare lists of columns:
struct Player : RowConvertible, Persistable { let name: String let score: Int static let databaseTableName = "players" // Declare Player columns enum Columns { static let name = Column("name") static let score = Column("score") } // Use columns in `init(row:)` init(row: Row) { name = row.value(Columns.name) score = row.value(Columns.score) } // Use columns in the new `encode(to:)` method: func encode(to container: inout PersistenceContainer) { container[Columns.name] = name container[Columns.score] = score } }
Database Observation has been enhanced:
Database.afterNextTransactionCommit(_:)
is the simplest way to handle successful transactions, and synchronize the database with other resources such as files, or system sensors (documentation).// Make sure the database is inside a transaction db.inSavepoint { // Perform some database job try ... // Register extra job that is only executed after database changes // have been committed and written to disk. db.afterNextTransactionCommit { ... } }
On the low-level side, applications can now specify the extent of database observation (documentation).
โ DatabaseMigrator is easier to test, with its
DatabaseMigrator.migrate(_:upTo:)
method which partially migrates your databases (documentation).On the side of database schema introspection, the new
Database.foreignKeys(on:)
method lists the foreign keys defined on a table.
Full list of changes
class Database { - func add(transactionObserver: TransactionObserver) + func add(transactionObserver: TransactionObserver, extent: TransactionObservationExtent = .observerLifetime) + func afterNextTransactionCommit(_ closure: @escaping (Database) -> ()) + func foreignKeys(on tableName: String) throws -> [ForeignKeyInfo] } -struct DatabaseCoder: DatabaseValueConvertible struct DatabaseMigrator { + func migrate(_ writer: DatabaseWriter, upTo targetIdentifier: String) throws } protocol DatabaseWriter : DatabaseReader { - func add(transactionObserver: TransactionObserver) + func add(transactionObserver: TransactionObserver, extent: TransactionObservationExtent = .observerLifetime) } protocol MutablePersistable : TableMapping { - var persistentDictionary: [String: DatabaseValueConvertible?] { get } + func encode(to container: inout PersistenceContainer) } extension QueryInterfaceRequest { - func contains(_ value: SQLExpressible) -> SQLExpression - func exists() -> SQLExpression } extension Request { - func adapted(_ adapter: @escaping (Database) throws -> RowAdapter) -> AnyRequest + func adapted(_ adapter: @escaping (Database) throws -> RowAdapter) -> AdaptedRequest<Self> } protocol TypedRequest : Request { - associatedtype Fetched + associatedtype RowDecoder } extension TypedRequest { - func adapted(_ adapter: @escaping (Database) throws -> RowAdapter) -> AnyTypedRequest<Fetched> + func adapted(_ adapter: @escaping (Database) throws -> RowAdapter) -> AdaptedTypedRequest<Self> }
-
v0.110.0 Changes
๐ Released May 28, 2017
๐ New
- โฌ๏ธ Upgrade custom SQLite builds to v3.19.2 (thanks to @swiftlyfalling).
- ๐ Upgrade SQLCipher to v3.4.1 (announcement, changelog)
- โ A large test suite that runs on Travis-CI, thanks to @swiftlyfalling in PR #213
๐ Fixed
- โ Remove deprecation warning about
sqlite3_trace
() for custom SQLite builds (addresses #100)
-
v0.109.0 Changes
๐ Released May 22, 2017
๐ Fixed
- Failed value conversions now consistently crash with a fatal error.
๐ New
DatabaseValue.losslessConvert()
performs a lossless conversion to a value type.
๐ฅ Breaking Changes
- ๐
DatabaseEventKind.impacts(_ selectionInfo:SelectStatement.SelectionInfo)
now returns an non-optional boolean. DatabaseWriter.availableDatabaseConnection
has been replaced byDatabaseWriter.unsafeReentrantWrite()
.Request.bound(to:)
has been renamedRequest.asRequest(of:)
.
-
v0.108.0 Changes
๐ Released May 17, 2017
๐ New
๐ Use CocoaPods to install GRDB with SQLCipher:
pod 'GRDBCipher'
๐ฅ Breaking Changes
- ๐
RowConvertible.awakeFromFetch()
has been removed.