Simple Schema Diff Viewer Clone

A minimal working clone of the Schema visual diff tool using Python and SQLite

Welcome to another official UVF IT build guide. In the next 45 minutes you’ll create a lightweight clone of the Schema diff viewer, letting you compare two SQLite schema files and see a colored HTML diff.

We’ll use Python’s built‑in sqlite3 and the tiny difflib library—both free and zero‑config—so you can run everything locally without any paid services.

Prerequisites & Gotchas

Clone Architecture
LOCAL RUNTIME read read serve HTML Python Diff Server Schema SQLite A Schema SQLite B HTML Diff UI
1

Phase 1: Scaffold the Project

Create a fresh folder for the clone and initialise a minimal Python environment. A virtual environment keeps dependencies isolated and makes the project portable across machines.

We’ll also add a simple requirements.txt even though the core libraries are built‑in; this mirrors real‑world projects and gives you a place to pin future packages.

mkdir schema-diff-clone
cd schema-diff-clone
python3 -m venv .venv
source .venv/bin/activate
# Upgrade pip and freeze an empty requirements file
pip install --upgrade pip
printf "# Add future deps here\n" > requirements.txt
2

Phase 2: Load & Serialize Schemas

Read two SQLite files and dump their CREATE statements into plain‑text strings. This gives us a deterministic representation of each schema that we can later feed to a diff algorithm.

The script loader.py opens each database, queries the sqlite_master table for objects of type table, index, trigger and view, and concatenates the SQL definitions in a stable order.

loader.py

import sqlite3
import sys

def extract_schema(db_path: str) -> str:
    """Return a single string with all CREATE statements from a SQLite file.
    Objects are sorted alphabetically to ensure deterministic output.
    """
    conn = sqlite3.connect(db_path)
    cur = conn.cursor()
    cur.execute(
        "SELECT type, name, sql FROM sqlite_master "
        "WHERE sql NOT NULL ORDER BY type, name;"
    )
    statements = []
    for obj_type, name, sql in cur.fetchall():
        statements.append(f"-- {obj_type.upper()}: {name}\n{sql.strip()};\n")
    conn.close()
    return "\n".join(statements)

if __name__ == "__main__":
    if len(sys.argv) != 3:
        print("Usage: python loader.py <db1.sqlite> <db2.sqlite>")
        sys.exit(1)
    schema_a = extract_schema(sys.argv[1])
    schema_b = extract_schema(sys.argv[2])
    # For now just print the raw schemas; later phases will diff them.
    print("--- Schema A ---")
    print(schema_a)
    print("--- Schema B ---")
    print(schema_b)
3

Phase 3: Generate HTML Diff

In this step we turn the raw schema strings into a visual side‑by‑side diff using Python's built‑in difflib.HtmlDiff. The helper script reads two SQLite files, extracts their CREATE statements, and feeds the two text blocks to HtmlDiff. The result is a self‑contained HTML snippet that highlights additions, deletions, and changes, making it easy to spot differences at a glance.

differ.py

import sys, sqlite3, difflib

def extract_schema(db_path):
    conn = sqlite3.connect(db_path)
    cur = conn.cursor()
    cur.execute("SELECT sql FROM sqlite_master WHERE sql NOT NULL ORDER BY name")
    rows = cur.fetchall()
    conn.close()
    # Join each CREATE statement on a new line
    return "\n".join(row[0] for row in rows)

def generate_html_diff(schema_a, schema_b):
    diff = difflib.HtmlDiff(wrapcolumn=80)
    return diff.make_file(schema_a.splitlines(), schema_b.splitlines(),
                         fromdesc='Schema A', todesc='Schema B')

if __name__ == "__main__":
    if len(sys.argv) != 3:
        print("Usage: python differ.py <db_a.sqlite> <db_b.sqlite>")
        sys.exit(1)
    a, b = sys.argv[1], sys.argv[2]
    html = generate_html_diff(extract_schema(a), extract_schema(b))
    print(html)
4

Phase 4: Simple Flask UI

Now we wrap the diff generator in a tiny web interface so anyone can upload two SQLite files and instantly see the differences. The Flask app serves a single HTML page with a file‑upload form, calls differ.py internally, and returns the generated HTML directly to the browser. Keeping the UI minimal helps you focus on the core functionality without getting bogged down in styling.

The endpoint validates that both files are present, stores them temporarily in memory, and reuses the same functions from differ.py to avoid duplication. All responses are plain HTML, so you can open the result in any modern browser.

app.py

from flask import Flask, request, render_template_string
import sqlite3, difflib, io

app = Flask(__name__)

HTML_TEMPLATE = '''
<!doctype html>
<title>Schema Diff Viewer</title>
<h2>Upload two SQLite databases</h2>
<form method=post enctype=multipart/form-data>
  <input type=file name=db_a required> <br><br>
  <input type=file name=db_b required> <br><br>
  <input type=submit value='Show Diff'>
</form>
''' 

def extract_schema(file_storage):
    # file_storage is a Werkzeug FileStorage object
    data = file_storage.read()
    conn = sqlite3.connect(':memory:')
    conn.executescript(data.decode('utf-8')) if data else None
    cur = conn.cursor()
    cur.execute("SELECT sql FROM sqlite_master WHERE sql NOT NULL ORDER BY name")
    rows = cur.fetchall()
    conn.close()
    return "\n".join(row[0] for row in rows)

def generate_html_diff(a_schema, b_schema):
    diff = difflib.HtmlDiff(wrapcolumn=80)
    return diff.make_file(a_schema.splitlines(), b_schema.splitlines(),
                         fromdesc='Schema A', todesc='Schema B')

@app.route('/', methods=['GET', 'POST'])
def index():
    if request.method == 'POST':
        if 'db_a' not in request.files or 'db_b' not in request.files:
            return 'Both files are required', 400
        a_schema = extract_schema(request.files['db_a'])
        b_schema = extract_schema(request.files['db_b'])
        html = generate_html_diff(a_schema, b_schema)
        return html
    return render_template_string(HTML_TEMPLATE)

if __name__ == '__main__':
    app.run(debug=True)
5

Phase 5: Run & Verify Locally

With the server code in place, start the Flask development server and point your browser at the local address. The UI will prompt you for two .sqlite files; after uploading, you should see a colored side‑by‑side diff rendered directly in the page. This confirms that the end‑to‑end flow—from file upload to HTML diff generation—works as intended.

export FLASK_APP=app.py
flask run

Verifying the Stack

CheckCommand / Why it matters
Server starts without errorsflask run displays *Running on http://127.0.0.1:5000/*
Upload two .sqlite files via the web formBrowser shows a side‑by‑side colored diff of the schemas
HTML diff contains <table> with class 'diff'Inspect page source to confirm difflib.HtmlDiff output

What’s Next?

You now have a functional schema diff viewer. Extend it by adding support for PostgreSQL/MySQL dumps, improve styling with CSS, or integrate Git hooks to diff migrations automatically.