This package provides undo-redo features for projects using GRDB. The undo-redo log is generated and managed through SQLite triggers.
Through Swift Package Manager.
Let's say you have this GRDB record:
struct Book: Identifiable, Equatable, Hashable {
var id: Int64? = nil
var title: String
var year: Int
var author: String
}
/// And its migration:
migrator.registerMigration("createRecords") { db in
try db.create(table: "books") { t in
t.autoIncrementedPrimaryKey("id")
t.column("title", .text).notNull().unique()
t.column("year", .integer).notNull()
t.column("author", .text).notNull()
}
}
Then, we advise to use the database manager to hold the UndoRedoManager instance.
class DatabaseManager {
private let dbQueue: DatabaseQueue
private let undoRedo: UndoRedoManager
init(dbQueue: DatabaseQueue) throws {
self.dbQueue = dbQueue
self.undoRedo = try UndoRedoManager(recordTypes: Book.self, db: /*your GRDB database queue or pool*/)
}
You can handle more than one table / record type: self.undoRedo = try UndoRedoManager(recordTypes: Book.self, Author.self, Editor.self, db: ...)
.
Together, the tables watched by UndoRedoManager form a "undo-redo scope". If the tables included in the scope have foreign key relationships to each other, see "Foreign Keys" below.
After each "step" (an action in your application), call try undoRedo.barrier()
. It can then be undone by calling try undoRedo.perform(.undo)
(then, .redo
).
extension DatabaseManager {
/// Saves (inserts or updates) a book. When the method returns, the
/// book is present in the database, and its id is not nil.
func saveBook(_ player: inout Book) throws {
/*validate that the book title, year, author*/
// save to database
try dbWriter.write { db in
try book.save(db)
}
// mark an atomic step, that can be un-done then possibly re-done
try self.undoRedo.barrier()
}
}
A step can include more than one database transaction. Be careful, that all changes between two calls to .barrier()
will be grouped in one step.
If your database can receive changes without user action, e.g. through background network calls, be careful not to let the user accidentally undo these! You can tell UndoRedoManager
to stop recording database changes via .freeze()
/ then .unfreeze()
. However: this library has no understanding of your application logic, so be careful regarding data consistency.
If foreign keys enforcement is enabled, UndoRedoManager.init
will ensure that no table, that is related through foreign keys to tables included in the undo-redo scope, are omitted; and will raise in such a case. If a database operation has cascade effects, all the changes will be included in the same step, and thus will reverted together on undo.
It is possible to supply a prefix to UndoRedoManager.init
, so that multiple instances can handle undo-redo for non-overlapping scopes in the same database. Having overlapping scopes will result in unpredictable consequences and possibly inconsistent data.
It is largely based on the example code available on the SQLite website. This translation to C++ and Python proved useful in understanding and translating the code to Swift.