Zero-dependency, trigger-based audit logging for SQLite. Works across any process or language writing to the same DB.
1
stars
5
commits
Python
primary language
Aug 28, 2026
updated
Zero-dependency, trigger-based audit logging for SQLite databases. Automatically tracks INSERT, UPDATE, and DELETE operations as structured JSON diffs — no ORM, no schema changes, no external services.
Existing ecosystem tools like sqlite-utils require running CLI commands out-of-band or manually computing diffs inside application code. sqlite-diff-log takes a different approach: it delegates diff generation to SQLite's own C core using native AFTER triggers and json_object().
sqlite3, json).sqlite3 CLI. You attach the trigger once; every writer downstream is covered.sqlite_master before use; all literals are escaped.pip install sqlite-diff-log
import sqlite3
from sqlite_diff_log import SQLiteDiffLog
conn = sqlite3.connect("app.db")
audit = SQLiteDiffLog(conn)
# Attach triggers to a table (pk_col defaults to "id")
audit.attach_to_table("users", pk_col="id")
# Any INSERT / UPDATE / DELETE on "users" — from this process
# or any other process writing to the same database file —
# is captured automatically from this point on.
with conn:
conn.execute("UPDATE users SET role = 'admin' WHERE id = 42")
# Query structured diffs
diffs = audit.get_logs("users")
print(diffs[0]["action"]) # "UPDATE"
print(diffs[0]["old_data"]) # {'id': 42, 'name': 'Alice', 'role': 'user'}
print(diffs[0]["new_data"]) # {'id': 42, 'name': 'Alice', 'role': 'admin'}
+------------------+ SQL Operations +--------------------+
| Any Writer | ---------------------------> | Target Table |
| (Python, Node, | (INSERT / UPDATE / DELETE) +--------------------+
| Go, sqlite3 CLI…)| |
+------------------+ Native SQLite Triggers
|
v
+--------------------+ JSON Diffs
| _audit_log Table | <----------------
+--------------------+
^
|
+--------------------+
| Audit Reader API |
+--------------------+
attach_to_table() installs three AFTER triggers (INSERT, UPDATE, DELETE) directly in SQLite. Each trigger fires inside the same transaction as the write it audits and inserts a JSON snapshot of the old and/or new row into _audit_log. Because the triggers are part of the database schema itself — not application code — they apply to every writer, not just the process that called attach_to_table().
| Column | Type | Description |
|---|---|---|
id | INTEGER | Autoincrementing primary key |
table_name | TEXT | Name of the audited table |
action | TEXT | INSERT, UPDATE, or DELETE |
row_id | ANY | Primary key value of the affected row |
old_data | TEXT/JSON | Row state before the change (NULL on INSERT) |
new_data | TEXT/JSON | Row state after the change (NULL on DELETE) |
created_at | TIMESTAMP | When the change was recorded |
SQLiteDiffLog(conn: sqlite3.Connection)Wraps an existing connection and creates the _audit_log table if it doesn't exist.
attach_to_table(table_name: str, pk_col: str = "id") -> NoneInstalls INSERT / UPDATE / DELETE triggers on table_name. Idempotent — safe to call multiple times (CREATE TRIGGER IF NOT EXISTS). Raises ValueError if the table doesn't exist or pk_col isn't a valid column.
get_logs(table_name: Optional[str] = None) -> List[Dict[str, Any]]Returns audit entries as a list of dicts, with old_data / new_data already parsed from JSON. Pass a table name to filter, or omit to get every audited table.
id). Composite primary keys are not currently supported.OLD.* / NEW.*), not who made the change or from which process — SQLite triggers have no concept of a connected user or session. If you need "who changed this," you'll need to pass that context into the row data yourself (e.g. an updated_by column) or handle it at the application layer.attach_to_table again) so the generated json_object() calls reflect the current schema.python -m unittest test_sqlite_diff_log.py
MIT — see LICENSE.
MigMarGil
5 commits
Python
100.0%
Zero-dependency, trigger-based audit logging for SQLite. Works across any process or language writing to the same DB.
1
stars
5
commits
Python
primary language
Aug 28, 2026
updated
Zero-dependency, trigger-based audit logging for SQLite databases. Automatically tracks INSERT, UPDATE, and DELETE operations as structured JSON diffs — no ORM, no schema changes, no external services.
Existing ecosystem tools like sqlite-utils require running CLI commands out-of-band or manually computing diffs inside application code. sqlite-diff-log takes a different approach: it delegates diff generation to SQLite's own C core using native AFTER triggers and json_object().
sqlite3, json).sqlite3 CLI. You attach the trigger once; every writer downstream is covered.sqlite_master before use; all literals are escaped.pip install sqlite-diff-log
import sqlite3
from sqlite_diff_log import SQLiteDiffLog
conn = sqlite3.connect("app.db")
audit = SQLiteDiffLog(conn)
# Attach triggers to a table (pk_col defaults to "id")
audit.attach_to_table("users", pk_col="id")
# Any INSERT / UPDATE / DELETE on "users" — from this process
# or any other process writing to the same database file —
# is captured automatically from this point on.
with conn:
conn.execute("UPDATE users SET role = 'admin' WHERE id = 42")
# Query structured diffs
diffs = audit.get_logs("users")
print(diffs[0]["action"]) # "UPDATE"
print(diffs[0]["old_data"]) # {'id': 42, 'name': 'Alice', 'role': 'user'}
print(diffs[0]["new_data"]) # {'id': 42, 'name': 'Alice', 'role': 'admin'}
+------------------+ SQL Operations +--------------------+
| Any Writer | ---------------------------> | Target Table |
| (Python, Node, | (INSERT / UPDATE / DELETE) +--------------------+
| Go, sqlite3 CLI…)| |
+------------------+ Native SQLite Triggers
|
v
+--------------------+ JSON Diffs
| _audit_log Table | <----------------
+--------------------+
^
|
+--------------------+
| Audit Reader API |
+--------------------+
attach_to_table() installs three AFTER triggers (INSERT, UPDATE, DELETE) directly in SQLite. Each trigger fires inside the same transaction as the write it audits and inserts a JSON snapshot of the old and/or new row into _audit_log. Because the triggers are part of the database schema itself — not application code — they apply to every writer, not just the process that called attach_to_table().
| Column | Type | Description |
|---|---|---|
id | INTEGER | Autoincrementing primary key |
table_name | TEXT | Name of the audited table |
action | TEXT | INSERT, UPDATE, or DELETE |
row_id | ANY | Primary key value of the affected row |
old_data | TEXT/JSON | Row state before the change (NULL on INSERT) |
new_data | TEXT/JSON | Row state after the change (NULL on DELETE) |
created_at | TIMESTAMP | When the change was recorded |
SQLiteDiffLog(conn: sqlite3.Connection)Wraps an existing connection and creates the _audit_log table if it doesn't exist.
attach_to_table(table_name: str, pk_col: str = "id") -> NoneInstalls INSERT / UPDATE / DELETE triggers on table_name. Idempotent — safe to call multiple times (CREATE TRIGGER IF NOT EXISTS). Raises ValueError if the table doesn't exist or pk_col isn't a valid column.
get_logs(table_name: Optional[str] = None) -> List[Dict[str, Any]]Returns audit entries as a list of dicts, with old_data / new_data already parsed from JSON. Pass a table name to filter, or omit to get every audited table.
id). Composite primary keys are not currently supported.OLD.* / NEW.*), not who made the change or from which process — SQLite triggers have no concept of a connected user or session. If you need "who changed this," you'll need to pass that context into the row data yourself (e.g. an updated_by column) or handle it at the application layer.attach_to_table again) so the generated json_object() calls reflect the current schema.python -m unittest test_sqlite_diff_log.py
MIT — see LICENSE.
MigMarGil
5 commits
Python
100.0%