add sequential migration system with version tracking
Replace the simple schema.sql runner with a proper sequential migration system that tracks applied versions in a _migrations table. Absorb scripts/migrate_to_paddle.py as versions/0001. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,19 +0,0 @@
|
||||
"""One-time migration: rename lemonsqueezy columns to paddle."""
|
||||
import sqlite3
|
||||
import sys
|
||||
|
||||
db_path = sys.argv[1] if len(sys.argv) > 1 else "data/app.db"
|
||||
conn = sqlite3.connect(db_path)
|
||||
conn.execute(
|
||||
"ALTER TABLE subscriptions RENAME COLUMN lemonsqueezy_customer_id TO paddle_customer_id"
|
||||
)
|
||||
conn.execute(
|
||||
"ALTER TABLE subscriptions RENAME COLUMN lemonsqueezy_subscription_id TO paddle_subscription_id"
|
||||
)
|
||||
conn.execute("DROP INDEX IF EXISTS idx_subscriptions_provider")
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_subscriptions_provider ON subscriptions(paddle_subscription_id)"
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
print(f"Migration complete: {db_path}")
|
||||
@@ -1,47 +1,99 @@
|
||||
"""
|
||||
Simple migration runner. Runs schema.sql against the database.
|
||||
Sequential migration runner.
|
||||
|
||||
- Runs schema.sql (idempotent CREATE IF NOT EXISTS for fresh DBs)
|
||||
- Scans versions/ for NNNN_*.py files and runs unapplied ones in order
|
||||
- Fresh DBs: marks all versions as applied without running them
|
||||
(schema.sql already contains the final schema)
|
||||
"""
|
||||
|
||||
import importlib
|
||||
import os
|
||||
import re
|
||||
import sqlite3
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add parent to path for imports
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
VERSIONS_DIR = Path(__file__).parent / "versions"
|
||||
VERSION_RE = re.compile(r"^(\d{4})_.+\.py$")
|
||||
|
||||
|
||||
def _discover_versions():
|
||||
"""Return sorted list of (name, module_path) for version files."""
|
||||
if not VERSIONS_DIR.is_dir():
|
||||
return []
|
||||
versions = []
|
||||
for f in sorted(VERSIONS_DIR.iterdir()):
|
||||
if VERSION_RE.match(f.name):
|
||||
versions.append(f.stem) # e.g. "0001_rename_ls_to_paddle"
|
||||
return versions
|
||||
|
||||
|
||||
def _is_fresh_db(conn):
|
||||
"""A DB is fresh if it has no application tables at all."""
|
||||
row = conn.execute(
|
||||
"SELECT 1 FROM sqlite_master WHERE type='table'"
|
||||
" AND name NOT LIKE 'sqlite_%'"
|
||||
).fetchone()
|
||||
return row is None
|
||||
|
||||
|
||||
def migrate():
|
||||
"""Run migrations."""
|
||||
# Get database path from env or default
|
||||
db_path = os.getenv("DATABASE_PATH", "data/app.db")
|
||||
|
||||
# Ensure directory exists
|
||||
Path(db_path).parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Read schema
|
||||
schema_path = Path(__file__).parent / "schema.sql"
|
||||
schema = schema_path.read_text()
|
||||
|
||||
# Connect and execute
|
||||
conn = sqlite3.connect(db_path)
|
||||
|
||||
# Enable WAL mode
|
||||
conn.execute("PRAGMA journal_mode=WAL")
|
||||
conn.execute("PRAGMA foreign_keys=ON")
|
||||
|
||||
# Run schema
|
||||
is_fresh = _is_fresh_db(conn)
|
||||
|
||||
# schema.sql is always idempotent (CREATE IF NOT EXISTS)
|
||||
schema = (Path(__file__).parent / "schema.sql").read_text()
|
||||
conn.executescript(schema)
|
||||
|
||||
versions = _discover_versions()
|
||||
applied = {
|
||||
row[0]
|
||||
for row in conn.execute("SELECT name FROM _migrations").fetchall()
|
||||
}
|
||||
pending = [v for v in versions if v not in applied]
|
||||
|
||||
if is_fresh:
|
||||
# Fresh DB — schema.sql already created final schema.
|
||||
# Record all versions as applied without executing them.
|
||||
for name in pending:
|
||||
conn.execute("INSERT INTO _migrations (name) VALUES (?)", (name,))
|
||||
conn.commit()
|
||||
print(f"✓ Fresh database initialised: {db_path}")
|
||||
if pending:
|
||||
print(f" Recorded {len(pending)} migration(s) as already applied")
|
||||
elif pending:
|
||||
for name in pending:
|
||||
print(f" Applying {name}...")
|
||||
mod = importlib.import_module(
|
||||
f"padelnomics.migrations.versions.{name}"
|
||||
)
|
||||
mod.up(conn)
|
||||
conn.execute(
|
||||
"INSERT INTO _migrations (name) VALUES (?)", (name,)
|
||||
)
|
||||
conn.commit()
|
||||
print(f"✓ Applied {len(pending)} migration(s): {db_path}")
|
||||
else:
|
||||
print(f"✓ All migrations already applied: {db_path}")
|
||||
|
||||
print(f"✓ Migrations complete: {db_path}")
|
||||
|
||||
# Show tables
|
||||
# Show tables (excluding internal sqlite/fts tables)
|
||||
cursor = conn.execute(
|
||||
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
|
||||
"SELECT name FROM sqlite_master WHERE type='table'"
|
||||
" AND name NOT LIKE 'sqlite_%'"
|
||||
" ORDER BY name"
|
||||
)
|
||||
tables = [row[0] for row in cursor.fetchall()]
|
||||
print(f" Tables: {', '.join(tables)}")
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
-- Padelnomics Database Schema
|
||||
-- Run with: python -m padelnomics.migrations.migrate
|
||||
|
||||
-- Migration tracking
|
||||
CREATE TABLE IF NOT EXISTS _migrations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT UNIQUE NOT NULL,
|
||||
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
-- Users
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
"""Rename lemonsqueezy columns to paddle."""
|
||||
|
||||
|
||||
def up(conn):
|
||||
conn.execute(
|
||||
"ALTER TABLE subscriptions"
|
||||
" RENAME COLUMN lemonsqueezy_customer_id TO paddle_customer_id"
|
||||
)
|
||||
conn.execute(
|
||||
"ALTER TABLE subscriptions"
|
||||
" RENAME COLUMN lemonsqueezy_subscription_id TO paddle_subscription_id"
|
||||
)
|
||||
conn.execute("DROP INDEX IF EXISTS idx_subscriptions_provider")
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_subscriptions_provider"
|
||||
" ON subscriptions(paddle_subscription_id)"
|
||||
)
|
||||
Reference in New Issue
Block a user