DumpTales

Finding what changed between database dumps

Two SQL dumps can tell you what a database looked like on Tuesday and Thursday. DumpTales tells you what happened in between, as far as those two snapshots can show.

DumpTales duck and database logo

A question between backups

I have often had two dumps of the same database and wanted a quick answer to a very specific question: which records appeared, disappeared or changed? Looking through thousands of lines of INSERT statements with a text diff gets tiring, especially if rows move around or the dump format changes.

DumpTales reads snapshots locally and compares rows by primary key. It can report supported changes to table columns, primary keys and foreign keys as well. It reads conventional MySQL and MariaDB SQL dumps, experimental PostgreSQL COPY dumps, and native SQLite database files. It does not execute the SQL or connect to a database server.

A tiny example

Here are two deliberately small MySQL dumps. The second upgrades Acme's plan, removes Old Co, and adds New Co. Harbour stays as it was.

before.sql

CREATE TABLE `accounts` (
  `id` int NOT NULL,
  `name` varchar(80) NOT NULL,
  `plan` varchar(20) NOT NULL,
  PRIMARY KEY (`id`)
);
INSERT INTO `accounts` VALUES (1,'Acme','basic'),(2,'Harbour','basic'),(3,'Old Co','trial');

after.sql

CREATE TABLE `accounts` (
  `id` int NOT NULL,
  `name` varchar(80) NOT NULL,
  `plan` varchar(20) NOT NULL,
  PRIMARY KEY (`id`)
);
INSERT INTO `accounts` VALUES (1,'Acme','pro'),(2,'Harbour','basic'),(4,'New Co','trial');

You can download before.sql and after.sql to try the example yourself:

dumptales before.sql after.sql --color always

This is the actual output from those files, with its terminal colours represented on the page:

~ accounts ['1']
    plan: 'basic' → 'pro'
- accounts ['3']
+ accounts ['4']
Summary: added=1, removed=1, changed=1, schema=0 (engine=stream, parser=python)

The yellow ~ marks a changed row and shows the field values. Red - and green + mark removed and added keys. This is a snapshot comparison: if a primary key changes, the result is a removal and an addition. The output does not claim to know what sequence of SQL statements caused the difference.

Working with bigger dumps

DumpTales inspects both inputs to choose its MySQL, PostgreSQL or SQLite reader. If it cannot recognise a dump, or the two inputs appear to use different dialects, it stops and asks you to specify --dialect explicitly. The same command works for a conventional plain-text pg_dump as for the MySQL example above:

dumptales forgejo-before.sql forgejo-after.sql

Compression is detected from the file contents, regardless of the filename. Plain SQL, gzip, bzip2 and ZIP archives containing exactly one SQL dump can be compared directly. Native SQLite database files need to remain uncompressed. A PostgreSQL custom-format archive made with pg_dump -Fc is a different format from a ZIP file and is outside the supported inputs.

For MySQL dumps in compatible primary-key order, DumpTales streams through both inputs. When rows are out of order, its automatic engine falls back to compressed hash partitions on temporary disk. There are JSON and JSONL output formats if you want to inspect changes with another tool:

dumptales old.sql.gz new.sql.gz --format json | jq

This yields:

{
  "version": "0.1.2",
  "changes": [
    {
      "kind": "changed",
      "table": "accounts",
      "key": [
        "1"
      ],
      "fields": {
        "plan": {
          "before": "basic",
          "after": "pro"
        }
      }
    },
    {
      "kind": "removed",
      "table": "accounts",
      "key": [
        "3"
      ],
      "before": {
        "id": "3",
        "name": "Old Co",
        "plan": "trial"
      }
    },
    {
      "kind": "added",
      "table": "accounts",
      "key": [
        "4"
      ],
      "after": {
        "id": "4",
        "name": "New Co",
        "plan": "trial"
      }
    }
  ],
  "summary": {
    "counts": {
      "changed": 1,
      "removed": 1,
      "added": 1
    },
    "skipped_tables": [],
    "unchanged_tables_skipped": [],
    "old_rows": 3,
    "new_rows": 3,
    "engine": "stream",
    "parser": "native",
    "relationships_complete": true
  }
}

There is a shortcut for identical MySQL table sections, and an optional canonical snapshot cache if you compare the same dumps repeatedly. Both use fingerprints to skip work. For an exhaustive parse, use --no-fast-skip. Keep your original SQL dumps as backups; the snapshot cache is for comparisons.

MySQL schema comparison covers parsed columns and primary and foreign keys, while indexes, triggers and views are outside its scope. The PostgreSQL reader is experimental and accepts a narrower range of pg_dump output. Tables without primary keys are counted as skipped; if every table lacks a detected primary key, DumpTales stops with an error instead of reporting zero changes. Check any skipped-table list before relying on the result. I would test representative dumps before relying on a result, particularly for PostgreSQL or unfamiliar SQL output.

I made this for those moments when the backup exists, the database may be elsewhere, and I just need to understand the change. The source and installation instructions are available on my Forgejo.

At a glance
  • Input: MySQL/MariaDB SQL dumps, experimental PostgreSQL COPY dumps, native SQLite files
  • Dialect: detected from both inputs; --dialect remains available for recognised formats that need an explicit choice
  • Compression: plain, gzip, bzip2 or single-dump ZIP for SQL; native SQLite files remain uncompressed
  • Output: coloured terminal summary, JSON, or JSONL
  • Comparison: primary-key rows and supported schema details
  • Processing: offline, read-only; temporary disk may be needed for unordered dumps
  • Install: pipx install dumptales (Python 3.10 or newer)
Need database performance or replication support?
I do contract systems and database administration, and can help.
Did you appreciate this article? Any support is appreciated!