Skip to main content

sqlite-diff-log

CI License: MIT PyPI Python

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.

Why this exists

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().

  • Zero runtime dependencies — built strictly with the Python standard library (sqlite3, json).
  • Language- and process-agnostic — because the logic lives entirely in SQLite triggers, not in Python code, any process that writes to the database gets audited automatically, whether it's another Python script, a Node.js service, a Go binary, or the sqlite3 CLI. You attach the trigger once; every writer downstream is covered.
  • SQL-injection protected — all identifiers (table names, column names, trigger names) are quoted and validated against sqlite_master before use; all literals are escaped.
  • Litestream / replication friendly — audit logs live inside a standard SQLite table, so they replicate transparently with tools like Litestream or Turso.

Installation

pip install sqlite-diff-log

Quick Start

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'}

How it works

+------------------+         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().

Audit log schema

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

API

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") -> None

Installs 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.

Limitations

  • Single-column primary keys only. Tables must use a single PK column (e.g. id). Composite primary keys are not currently supported.
  • No caller identity. Triggers only see row data (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.
  • Schema changes require re-attaching. If you add or drop columns on an audited table, drop and recreate the triggers (attach_to_table again) so the generated json_object() calls reflect the current schema.

Running Tests

python -m unittest test_sqlite_diff_log.py

License

MIT — see LICENSE.

Author

MigMarGil

Download files

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

Source Distribution

sqlite_diff_log-0.1.1.tar.gz (6.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

sqlite_diff_log-0.1.1-py3-none-any.whl (6.3 kB view details)

Uploaded Python 3

File details

Details for the file sqlite_diff_log-0.1.1.tar.gz.

File metadata

  • Download URL: sqlite_diff_log-0.1.1.tar.gz
  • Upload date:
  • Size: 6.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.14.4

File hashes

Hashes for sqlite_diff_log-0.1.1.tar.gz
Algorithm Hash digest
SHA256 b367c731e52909b76b835a6cecea9fddd6f2d80ed9cd38ffa3cb574510aec357
MD5 861f259960a4efeaf14624849e9fb6f2
BLAKE2b-256 dbbdb30acfd3addebab8428ce28536b106eb4f8dca1a1cb5a861a19565f1bbe6

See more details on using hashes here.

File details

Details for the file sqlite_diff_log-0.1.1-py3-none-any.whl.

File metadata

File hashes

Hashes for sqlite_diff_log-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 3022ca29ba0ada14ee1b7542d34edb29ee4ef67540ea46eb28dec5948e9f63a5
MD5 50031e71e23093d298b270085e80e925
BLAKE2b-256 e2e9488835919c4ef89d3a3e82873972bfe347d555a77328427881f3d51c6743

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page