Skip to main content

Track changes to SQLite tables using triggers

Project description

sqlite-history

PyPI Changelog License

Track changes to SQLite tables using triggers

Installation

Install this library using pip:

pip install sqlite-history

Usage

This library can be used to configure triggers on a SQLite database such that any inserts, updates or deletes against a table will have their changes recorded in a separate table.

You can enable history tracking for a table using the enable_history() function:

import sqlite_history
import sqlite3

conn = sqlite3.connect("data.db")
conn.execute("CREATE TABLE table1 (id INTEGER PRIMARY KEY, name TEXT)")
sqlite_history.configure_history(conn, "table1")

Or you can use the CLI interface, available via python -m sqlite_history:

python -m sqlite_history data.db table1 [table2 table3 ...]

Use --all to configure it for all tables:

python -m sqlite_history data.db --all

How this works

Given a table with the following schema:

CREATE TABLE people (
    id INTEGER PRIMARY KEY,
    name TEXT,
    age INTEGER,
    weight REAL
);

This library will create a new table called _people_history with the following schema:

CREATE TABLE _people_history (
    _rowid INTEGER,
   id INTEGER,
   name TEXT,
   age INTEGER,
   weight REAL,
    _version INTEGER,
    _updated INTEGER,
    _mask INTEGER
);
CREATE INDEX idx_people_history_rowid ON _people_history (_rowid);

The _rowid column references the rowid of the row in the original table that is being tracked. If a row has been updated multiple times there will be multiple rows with the same _rowid in this table.

The id, name, age and weight columns represent the new values assigned to the row when it was updated. These can also be null, which might represent no change or might represent the value being set to null (hence the _mask column).

The _version column is a monotonically increasing integer that is incremented each time a row is updated.

The _updated column is a timestamp showing when the change was recorded. This is stored in milliseconds since the Unix epoch - to convert that to a human-readable UTC date you can use strftime('%Y-%m-%d %H:%M:%S', _updated / 1000, 'unixepoch') in your SQL queries.

The _mask column is a bit mask that indicates which columns changed in an update. The bit mask is calculated by adding together the following values:

1: id
2: name
4: age
8: weight

Tables with different schemas will have different _mask values.

A _mask of -1 indicates that the row was deleted.

The following triggers are created to populate the _people_history table:

CREATE TRIGGER people_insert_history
AFTER INSERT ON people
BEGIN
    INSERT INTO _people_history (_rowid, id, name, age, weight, _version, _updated, _mask)
    VALUES (new.rowid, new.id, new.name, new.age, new.weight, 1, cast((julianday('now') - 2440587.5) * 86400 * 1000 as integer), 15);
END;

CREATE TRIGGER people_update_history
AFTER UPDATE ON people
FOR EACH ROW
BEGIN
    INSERT INTO _people_history (_rowid, id, name, age, weight, _version, _updated, _mask)
    SELECT old.rowid, 
        CASE WHEN old.id != new.id then new.id else null end, 
        CASE WHEN old.name != new.name then new.name else null end, 
        CASE WHEN old.age != new.age then new.age else null end, 
        CASE WHEN old.weight != new.weight then new.weight else null end,
        (SELECT MAX(_version) FROM _people_history WHERE _rowid = old.rowid) + 1,
        cast((julianday('now') - 2440587.5) * 86400 * 1000 as integer),
        (CASE WHEN old.id != new.id then 1 else 0 end) + (CASE WHEN old.name != new.name then 2 else 0 end) + (CASE WHEN old.age != new.age then 4 else 0 end) + (CASE WHEN old.weight != new.weight then 8 else 0 end)
    WHERE old.id != new.id or old.name != new.name or old.age != new.age or old.weight != new.weight;
END;

CREATE TRIGGER people_delete_history
AFTER DELETE ON people
BEGIN
    INSERT INTO _people_history (_rowid, id, name, age, weight, _version, _updated, _mask)
    VALUES (
        old.rowid,
        old.id, old.name, old.age, old.weight,
        (SELECT COALESCE(MAX(_version), 0) from _people_history WHERE _rowid = old.rowid) + 1,
        cast((julianday('now') - 2440587.5) * 86400 * 1000 as integer),
        -1
    );
END;

Development

To contribute to this library, first checkout the code. Then create a new virtual environment:

cd sqlite-history
python -m venv venv
source venv/bin/activate

Now install the dependencies and test dependencies:

pip install -e '.[test]'

To run the tests:

pytest

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

sqlite-history-0.1.tar.gz (10.8 kB view details)

Uploaded Source

Built Distribution

sqlite_history-0.1-py3-none-any.whl (11.1 kB view details)

Uploaded Python 3

File details

Details for the file sqlite-history-0.1.tar.gz.

File metadata

  • Download URL: sqlite-history-0.1.tar.gz
  • Upload date:
  • Size: 10.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.2 CPython/3.11.2

File hashes

Hashes for sqlite-history-0.1.tar.gz
Algorithm Hash digest
SHA256 d8d95776d6c633e4dbfd48ac59eca0f4f760bc2fcc63a2131c65efcffbdf5621
MD5 e8a73e1d6d665c22fbdf9b80f09ef91f
BLAKE2b-256 9d5faec91eebf8508a1e8d814ceb9bd27a09ee95245ebe22dbf3c3ba2db1ef79

See more details on using hashes here.

File details

Details for the file sqlite_history-0.1-py3-none-any.whl.

File metadata

File hashes

Hashes for sqlite_history-0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 bcfb10dae3307a08bcc07f95b67510d13132a3765c46f16ca4477de52e7bfe82
MD5 298354d54caa12262e8c18fb6ffd9214
BLAKE2b-256 0460aff893bea7b44caf54f9a30b55500b6bae449cac86025e0d1d8859fde6ad

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page