litellm/ci_cd/run_migration.py
Alexsander Hamir 6bf33b6673 Make migration generation idempotent by default
- Add shared utility function to make migrations idempotent (IF NOT EXISTS)
- Update migration generation scripts to use shared utility
- Fix existing migration file to use IF NOT EXISTS clauses
- Add comprehensive test suite with 23 test cases covering edge cases
- Ensure all future migrations are automatically idempotent

This prevents migration failures when columns/indexes already exist,
making migrations safe to re-run on databases that were manually fixed.
2026-02-03 10:32:09 -08:00

100 lines
3.6 KiB
Python

import os
import subprocess
from pathlib import Path
from datetime import datetime
import testing.postgresql
import shutil
from ci_cd.migration_utils import make_migration_idempotent
def create_migration(migration_name: str = None):
"""
Create a new migration SQL file in the migrations directory by comparing
current database state with schema
Args:
migration_name (str): Name for the migration
"""
try:
# Get paths
root_dir = Path(__file__).parent.parent
migrations_dir = root_dir / "litellm-proxy-extras" / "litellm_proxy_extras" / "migrations"
schema_path = root_dir / "schema.prisma"
# Create temporary PostgreSQL database
with testing.postgresql.Postgresql() as postgresql:
db_url = postgresql.url()
# Create temporary migrations directory next to schema.prisma
temp_migrations_dir = schema_path.parent / "migrations"
try:
# Copy existing migrations to temp directory
if temp_migrations_dir.exists():
shutil.rmtree(temp_migrations_dir)
shutil.copytree(migrations_dir, temp_migrations_dir)
# Apply existing migrations to temp database
os.environ["DATABASE_URL"] = db_url
subprocess.run(
["prisma", "migrate", "deploy", "--schema", str(schema_path)],
check=True,
)
# Generate diff between current database and schema
result = subprocess.run(
[
"prisma",
"migrate",
"diff",
"--from-url",
db_url,
"--to-schema-datamodel",
str(schema_path),
"--script",
],
capture_output=True,
text=True,
check=True,
)
if result.stdout.strip():
# Generate timestamp and create migration directory
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
migration_name = migration_name or "unnamed_migration"
migration_dir = migrations_dir / f"{timestamp}_{migration_name}"
migration_dir.mkdir(parents=True, exist_ok=True)
# Post-process SQL to make it idempotent
idempotent_sql = make_migration_idempotent(result.stdout)
# Write the SQL to migration.sql
migration_file = migration_dir / "migration.sql"
migration_file.write_text(idempotent_sql)
print(f"Created migration in {migration_dir}")
return True
else:
print("No schema changes detected. Migration not needed.")
return False
finally:
# Clean up: remove temporary migrations directory
if temp_migrations_dir.exists():
shutil.rmtree(temp_migrations_dir)
except subprocess.CalledProcessError as e:
print(f"Error generating migration: {e.stderr}")
return False
except Exception as e:
print(f"Error creating migration: {str(e)}")
return False
if __name__ == "__main__":
# If running directly, can optionally pass migration name as argument
import sys
migration_name = sys.argv[1] if len(sys.argv) > 1 else None
create_migration(migration_name)