Small. Fast. Reliable.

Choose any three. Home

Menu

About

Documentation

Download

License

Support

Purchase

Search About

Documentation

Download

Support

Purchase Search Documentation Search Changelog

Automatic Undo/Redo Using SQLite

This page demonstrates how to use triggers to implement undo/redo logic for an application that uses SQLite as its application file format.

Object-Oriented Design

This design note considers the database to be a collection of objects. Each SQL table is a class. Each row is an instance of that class. There are, of course, other ways to interpret an SQL database schema, and the techniques described here work equally well under alternative interpretations, but an object-oriented view seems be more natural to most contemporary programmers.

Capture Changes Using Triggers

The core idea is to create a special table (named "UNDOLOG" in the example) that holds information needed to undo/redo changes to the database. For each class (table) in the database that wants to participate in the undo/redo, triggers are created that cause entries to be made in the UNDOLOG table for each DELETE, INSERT, and UPDATE of the participating class. The UNDOLOG entries consist of ordinary SQL statements the can be played back to reverse the changes.

For example, suppose you wanted undo/redo on a class (table) that looks like this:

CREATE TABLE ex1(a,b,c);

Triggers to record changes to table EX1 might look like this:

CREATE TEMP TRIGGER ex1_it AFTER INSERT ON ex1 BEGIN INSERT INTO undolog VALUES(NULL,'DELETE FROM ex1 WHERE rowid='||new.rowid); END; CREATE TEMP TRIGGER ex1_ut AFTER UPDATE ON ex1 BEGIN INSERT INTO undolog VALUES(NULL,'UPDATE ex1 SET a='||quote(old.a)||',b='||quote(old.b)||',c='||quote(old.c)||' WHERE rowid='||old.rowid); END; CREATE TEMP TRIGGER ex1_dt BEFORE DELETE ON ex1 BEGIN INSERT INTO undolog VALUES(NULL,'INSERT INTO ex1(rowid,a,b,c) VALUES('||old.rowid||','||quote(old.a)||','||quote(old.b)|| ','||quote(old.c)||')'); END;

After each INSERT on ex1, the ex1_it trigger constructs text of a DELETE statement that will undo the INSERT. The ex1_ut trigger constructs an UPDATE statement that will undo the effects of an UPDATE. And the ex1_dt trigger constructs a statement that will undo the effects of a DELETE.

Note the use of the quote() SQL function in these triggers. The quote() function converts its argument into a form that is appropriate for inclusion in an SQL statement. Numeric values come through unchanged. Single quotes are added before and after strings and any internal single quotes are escaped. BLOB values are rendered using SQL-standard hexadecimal BLOB notation. The use of the quote() function ensures that the SQL statements used to undo and redo are always safe from SQL injection.

Automatic Creation Of Triggers

Triggers such as the above could be entered manually, but that is tedious. An important feature of the technique demonstrated below is that the triggers are generated automatically.

The implementation language for the example code is TCL, though you can easily do the same thing in another programming language. Remember that the code here is a demonstration of the technique, not a drop-in module that will automatically do everything for you. The demonstration code shown below is derived from actual code in production use. But you will need to make changes to tailor it to your application.

To activate the undo/redo logic, invoke the undo::activate command with all classes (tables) that are to participate in the undo/redo as arguments. Use undo::deactivate, undo::freeze, and undo::unfreeze to control the state of the undo/redo mechanism.

The undo::activate command creates temporary triggers in the database that record all changes made to the tables named in the arguments.

Application Interface

After a sequence of changes that define a single undo/redo step, invoke the undo::barrier command to define the limit of that step. In an interactive program, you can call undo::event after any change and undo::barrier will be called automatically as an idle callback.

When the user presses the Undo button, invoke undo::undo. Invoke undo::redo when the user presses the Redo button.

On each call to undo::undo or undo::redo, the undo/redo module automatically invokes methods status_refresh and reload_all in all toplevel namespaces. These methods should be defined to reconstruct the display or otherwise update the state of the program based on the undone/redone changes to the database.

The demonstration code below includes a status_refresh method that grays-out or activates the Undo and Redo buttons and menu entries depending on whether or not there is anything to be undone or redone. You will need to redefine this method to control the Undo and Redo buttons in your application.

The demonstration code assumes that the SQLite database is opened used as a database object named "db".

Example Code