воскресенье, 12 июля 2009 г.

Расширение UNDO для СУБД SQLite

Недавно Simon Naunton анонсировал расширение для SQLite, реализующие функционал undo/redo. Описание принципа работы соответствующего функционала известно давно и может быть найдено на страничке
http://www.sqlite.org/cvstrac/wiki?p=UndoRedo

Модуль предоставляет возможность отменять один запрос или набор запросов и повторять их после отмены. Таким образом, программирование действий "Отменить" и "Повторить" в приложении существенно упрощается.

Непосредственно реализация мне понравилась, хотя и содержала неекоторые странности - например, привязку к primary keys вместо использования rowid и проч. К счастью, автор быстро откликается на пожелания и предложения и уже подготовил новый релиз, список изменений можно посмотреть в файле ChangeLog архива исходного кода.

Остается еще несколько вещей, которые мне сильно не нравятся. В том числе:
- неявный вызов транзакции в функциях - требует переписывания существующих приложений без использования транзакций (sic!),
- функция undoable_table требует "лишний" параметр, который принципиально вообще не нужен, а на практике бесполезен и при неправильном использовании даже вреден (из-за возможности создания огромного числа триггеров на многоколоночных таблицах)
- функция undoable для выполнение SQL-строки - явно следует ее вынести на уровень приложения.

Не уверен, примет ли автор соответствующий патч - пока аргументирует отказ тем, что текущий вариант ему удобен. Автору-то, может быть, и удобно, если он писал приложения именно под свое расширение, а вот для новых или уже существующих приложений выкинуть транзакции явно неприемлемо. Посему выкладываю в репозиторий свою версию расширения, независимо от того, войдут ли эти изменения в апстрим.

Моя аргументация на данный момент выглядит следующим образом:

1. I think that existing application logic must be always saved. So I did remove hidden calls of BEGIN and COMMIT from sqlite_undo_undoable_begin_do and sqlite_undo_undoable_end functions.
Now I can using savepoints as nested transactions and all functionality of extension is saved.

2. I did remove second parameter from undoable_table('x', y) because for temp undo/redo stack size of undo log is not critical but a lot of triggers is not good solution by performance reasons.
Param value '0' is strange because is not usefull and doesn't perform any optimization really.

3. undoable('x') can be replaced by
select undoable_begin();
select perform ('x');
select undoable_end();

Where "perform" is your application function for evaluating SQL strings. I don't understand why you are include this functionality into public extension. I think more better to create application
function "undoable(x)" as described above. Of cource this is IMHO but more simple extension with same functionality is best.

Please remember that SQLite slogan is "Small. Fast. Reliable.Choose any three."! I think undo extension with these simplifications is more corresponded to this title. Patched file is attached.


Пример использования:

sqlite> CREATE TABLE test(name text, title text);
sqlite> select undoable_table('test');

sqlite> select undoable_begin();

sqlite> insert into test values ('master','admin');
sqlite> insert into test values ('veter','developer');
sqlite> select undoable_end();
UNDO=1
REDO=0
sqlite> select undo();
UNDO=0
REDO=1
SQL=DELETE FROM test WHERE rowid=1;DELETE FROM test WHERE rowid=2


Тот же пример, где добавлен вывод undo/redo информации из служебной таблицы _undo :

sqlite> CREATE TABLE test(name text, title text);
sqlite> select undoable_table('test');

sqlite> select undoable_begin();

sqlite> insert into test values ('master','admin');
sqlite> insert into test values ('veter','developer');
sqlite> select undoable_end();
UNDO=1
REDO=0

sqlite> select * from _undo;
U
DELETE FROM test WHERE rowid=1
DELETE FROM test WHERE rowid=2

sqlite> select undo();
UNDO=0
REDO=1
SQL=DELETE FROM test WHERE rowid=1;DELETE FROM test WHERE rowid=2

sqlite> select * from _undo;
R
INSERT INTO test(rowid,name,title) VALUES(1,'master','admin')
INSERT INTO test(rowid,name,title) VALUES(2,'veter','developer')


Настройки моего репозитория вы можете найти на следующей странице
http://mobigroup.ru/page/debian
или вручную скачать оригинальные исходники SQLite и патч-файл с моими изменениями здесь:
http://mobigroup.ru/debian/pool/main/s/sqlite3/

P.S. Аналогично можно реализовать ведение истории изменений БД. Приведу обсуждение этой возможности, вдруг кто из читателей захочет реализовать такой модуль. Впрочем, если будет время, постараюсь сам наконец добраться и сделать, поскольку иногда хочется иметь такой функционал.

> "How about changes logging extension by same way? ;-)"
>
> Do you mean having a _log table where all updates to the table are logged?
>
> e.g.
>
> create myloggedtable(data text);
> select
> logged_table('myloggedtable');
> insert into myloggedtable(data) ('test');
> insert into myloggedtable(data) ('test2');
> select rowid,* from _log
> > 1, INSERT INTO test(data) VALUES('test');
> > 2, INSERT INTO test(data) VALUES('test2');

Yes, but _log table must have table_name, table_rowid, save_date and
operation_name columns and consists _only insert_ commands. For history
review we may create temp table same as original and populate it with
all logged versions of data of original table or of the single row of this
table. Values of operation_name are insert|update|delete. Log records
for delete sql commands may have no sql. Thus _log table will let to
restore state of any database record of any time.

1 комментарий:

baron комментирует...

Очень жду постов на эту тему! Огромное вам Спасибо


(C) Alexey Pechnikov aka MBG, mobigroup.ru