From eccf4d1441a32be940697f397050a5999583b47f Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Fri, 27 Mar 2026 05:34:13 -0500 Subject: [PATCH] feat: add migration tracking system (#81) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds schema_versions table and migrations.py runner to prevent double-application and missed migrations across dev/prod environments. - migrations/2026-03-27_add_schema_versions_table.sql: creates tracking table - migrations.py: applies pending .sql files in sorted order, records each in schema_versions - .gitignore: untrack migrations.py (was incorrectly ignored as legacy root file) First run on an existing DB will apply all migrations (safe — all use IF NOT EXISTS). Closes #81 Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 1 - migrations.py | 88 +++++++++++++++++++ .../2026-03-27_add_schema_versions_table.sql | 9 ++ 3 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 migrations.py create mode 100644 migrations/2026-03-27_add_schema_versions_table.sql diff --git a/.gitignore b/.gitignore index 78f68bb..4f6baa9 100644 --- a/.gitignore +++ b/.gitignore @@ -55,7 +55,6 @@ Include/ pyvenv.cfg db_engine.py main.py -migrations.py db_engine.py sba_master.db db_engine.py diff --git a/migrations.py b/migrations.py new file mode 100644 index 0000000..b3d6840 --- /dev/null +++ b/migrations.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +"""Apply pending SQL migrations and record them in schema_versions. + +Usage: + python migrations.py + +Connects to PostgreSQL using the same environment variables as the API: + POSTGRES_DB (default: sba_master) + POSTGRES_USER (default: sba_admin) + POSTGRES_PASSWORD (required) + POSTGRES_HOST (default: sba_postgres) + POSTGRES_PORT (default: 5432) + +On first run against an existing database, all migrations will be applied. +All migration files use IF NOT EXISTS guards so re-applying is safe. +""" + +import os +import sys +from pathlib import Path + +import psycopg2 + + +MIGRATIONS_DIR = Path(__file__).parent / "migrations" + +_CREATE_SCHEMA_VERSIONS = """ +CREATE TABLE IF NOT EXISTS schema_versions ( + filename VARCHAR(255) PRIMARY KEY, + applied_at TIMESTAMP NOT NULL DEFAULT NOW() +); +""" + + +def _get_connection(): + password = os.environ.get("POSTGRES_PASSWORD") + if password is None: + raise RuntimeError("POSTGRES_PASSWORD environment variable is not set") + return psycopg2.connect( + dbname=os.environ.get("POSTGRES_DB", "sba_master"), + user=os.environ.get("POSTGRES_USER", "sba_admin"), + password=password, + host=os.environ.get("POSTGRES_HOST", "sba_postgres"), + port=int(os.environ.get("POSTGRES_PORT", "5432")), + ) + + +def main(): + conn = _get_connection() + try: + with conn: + with conn.cursor() as cur: + cur.execute(_CREATE_SCHEMA_VERSIONS) + + with conn.cursor() as cur: + cur.execute("SELECT filename FROM schema_versions") + applied = {row[0] for row in cur.fetchall()} + + migration_files = sorted(MIGRATIONS_DIR.glob("*.sql")) + pending = [f for f in migration_files if f.name not in applied] + + if not pending: + print("No pending migrations.") + return + + for migration_file in pending: + print(f"Applying {migration_file.name} ...", end=" ", flush=True) + sql = migration_file.read_text() + with conn: + with conn.cursor() as cur: + cur.execute(sql) + cur.execute( + "INSERT INTO schema_versions (filename) VALUES (%s)", + (migration_file.name,), + ) + print("done") + + print(f"\nApplied {len(pending)} migration(s).") + finally: + conn.close() + + +if __name__ == "__main__": + try: + main() + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) diff --git a/migrations/2026-03-27_add_schema_versions_table.sql b/migrations/2026-03-27_add_schema_versions_table.sql new file mode 100644 index 0000000..1fa01f1 --- /dev/null +++ b/migrations/2026-03-27_add_schema_versions_table.sql @@ -0,0 +1,9 @@ +-- Migration: Add schema_versions table for migration tracking +-- Date: 2026-03-27 +-- Description: Creates a table to record which SQL migrations have been applied, +-- preventing double-application and missed migrations across environments. + +CREATE TABLE IF NOT EXISTS schema_versions ( + filename VARCHAR(255) PRIMARY KEY, + applied_at TIMESTAMP NOT NULL DEFAULT NOW() +); -- 2.25.1