Add File
This commit is contained in:
655
src/landppt/database/migrations.py
Normal file
655
src/landppt/database/migrations.py
Normal file
@@ -0,0 +1,655 @@
|
|||||||
|
"""
|
||||||
|
Database migration utilities for LandPPT
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
from typing import List, Dict, Any
|
||||||
|
from sqlalchemy import text
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from .database import AsyncSessionLocal, async_engine
|
||||||
|
from .models import Base
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class DatabaseMigration:
|
||||||
|
"""Database migration manager"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.migrations = []
|
||||||
|
self._register_migrations()
|
||||||
|
|
||||||
|
def _register_migrations(self):
|
||||||
|
"""Register all available migrations"""
|
||||||
|
# Migration 001: Initial schema
|
||||||
|
self.migrations.append({
|
||||||
|
"version": "001",
|
||||||
|
"name": "initial_schema",
|
||||||
|
"description": "Create initial database schema",
|
||||||
|
"up": self._migration_001_up,
|
||||||
|
"down": self._migration_001_down
|
||||||
|
})
|
||||||
|
|
||||||
|
# Migration 002: Add indexes
|
||||||
|
self.migrations.append({
|
||||||
|
"version": "002",
|
||||||
|
"name": "add_indexes",
|
||||||
|
"description": "Add performance indexes",
|
||||||
|
"up": self._migration_002_up,
|
||||||
|
"down": self._migration_002_down
|
||||||
|
})
|
||||||
|
|
||||||
|
# Migration 003: Add project_id to todo_stages
|
||||||
|
self.migrations.append({
|
||||||
|
"version": "003",
|
||||||
|
"name": "add_project_id_to_todo_stages",
|
||||||
|
"description": "Add project_id column to todo_stages table for better indexing",
|
||||||
|
"up": self._migration_003_up,
|
||||||
|
"down": self._migration_003_down
|
||||||
|
})
|
||||||
|
|
||||||
|
# Migration 004: Add PPT templates table
|
||||||
|
self.migrations.append({
|
||||||
|
"version": "004",
|
||||||
|
"name": "add_ppt_templates_table",
|
||||||
|
"description": "Add PPT templates table and update slide_data table",
|
||||||
|
"up": self._migration_004_up,
|
||||||
|
"down": self._migration_004_down
|
||||||
|
})
|
||||||
|
|
||||||
|
# Migration 005: Add project_metadata column to projects table
|
||||||
|
self.migrations.append({
|
||||||
|
"version": "005",
|
||||||
|
"name": "add_project_metadata_to_projects",
|
||||||
|
"description": "Add project_metadata column to projects table for storing template selection and other metadata",
|
||||||
|
"up": self._migration_005_up,
|
||||||
|
"down": self._migration_005_down
|
||||||
|
})
|
||||||
|
|
||||||
|
# Migration 006: Add is_user_edited field to slide_data table
|
||||||
|
self.migrations.append({
|
||||||
|
"version": "006",
|
||||||
|
"name": "add_is_user_edited_to_slide_data",
|
||||||
|
"description": "Add is_user_edited field to slide_data table to track user manual edits",
|
||||||
|
"up": self._migration_006_up,
|
||||||
|
"down": self._migration_006_down
|
||||||
|
})
|
||||||
|
|
||||||
|
async def _migration_001_up(self, session: AsyncSession):
|
||||||
|
"""Create initial schema"""
|
||||||
|
logger.info("Running migration 001: Creating initial schema")
|
||||||
|
|
||||||
|
# Create all tables
|
||||||
|
async with async_engine.begin() as conn:
|
||||||
|
await conn.run_sync(Base.metadata.create_all)
|
||||||
|
|
||||||
|
logger.info("Migration 001 completed successfully")
|
||||||
|
|
||||||
|
async def _migration_001_down(self, session: AsyncSession):
|
||||||
|
"""Drop initial schema"""
|
||||||
|
logger.info("Rolling back migration 001: Dropping all tables")
|
||||||
|
|
||||||
|
async with async_engine.begin() as conn:
|
||||||
|
await conn.run_sync(Base.metadata.drop_all)
|
||||||
|
|
||||||
|
logger.info("Migration 001 rollback completed")
|
||||||
|
|
||||||
|
async def _migration_002_up(self, session: AsyncSession):
|
||||||
|
"""Add performance indexes"""
|
||||||
|
logger.info("Running migration 002: Adding performance indexes")
|
||||||
|
|
||||||
|
indexes = [
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_projects_status ON projects(status)",
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_projects_scenario ON projects(scenario)",
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_projects_created_at ON projects(created_at)",
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_todo_stages_status ON todo_stages(status)",
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_slide_data_slide_index ON slide_data(slide_index)",
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_project_versions_timestamp ON project_versions(timestamp)"
|
||||||
|
]
|
||||||
|
|
||||||
|
for index_sql in indexes:
|
||||||
|
await session.execute(text(index_sql))
|
||||||
|
|
||||||
|
await session.commit()
|
||||||
|
logger.info("Migration 002 completed successfully")
|
||||||
|
|
||||||
|
async def _migration_002_down(self, session: AsyncSession):
|
||||||
|
"""Remove performance indexes"""
|
||||||
|
logger.info("Rolling back migration 002: Removing performance indexes")
|
||||||
|
|
||||||
|
indexes = [
|
||||||
|
"DROP INDEX IF EXISTS idx_projects_status",
|
||||||
|
"DROP INDEX IF EXISTS idx_projects_scenario",
|
||||||
|
"DROP INDEX IF EXISTS idx_projects_created_at",
|
||||||
|
"DROP INDEX IF EXISTS idx_todo_stages_status",
|
||||||
|
"DROP INDEX IF EXISTS idx_slide_data_slide_index",
|
||||||
|
"DROP INDEX IF EXISTS idx_project_versions_timestamp"
|
||||||
|
]
|
||||||
|
|
||||||
|
for index_sql in indexes:
|
||||||
|
await session.execute(text(index_sql))
|
||||||
|
|
||||||
|
await session.commit()
|
||||||
|
logger.info("Migration 002 rollback completed")
|
||||||
|
|
||||||
|
async def _migration_003_up(self, session: AsyncSession):
|
||||||
|
"""Add project_id column to todo_stages table"""
|
||||||
|
logger.info("Running migration 003: Adding project_id to todo_stages")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Check if project_id column already exists
|
||||||
|
result = await session.execute(text("""
|
||||||
|
PRAGMA table_info(todo_stages)
|
||||||
|
"""))
|
||||||
|
columns = result.fetchall()
|
||||||
|
column_names = [col[1] for col in columns]
|
||||||
|
|
||||||
|
if 'project_id' not in column_names:
|
||||||
|
# Add project_id column to todo_stages table
|
||||||
|
await session.execute(text("""
|
||||||
|
ALTER TABLE todo_stages
|
||||||
|
ADD COLUMN project_id VARCHAR(36)
|
||||||
|
"""))
|
||||||
|
logger.info("Added project_id column to todo_stages")
|
||||||
|
else:
|
||||||
|
logger.info("project_id column already exists in todo_stages")
|
||||||
|
|
||||||
|
# Create index on project_id
|
||||||
|
await session.execute(text("""
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_todo_stages_project_id
|
||||||
|
ON todo_stages(project_id)
|
||||||
|
"""))
|
||||||
|
|
||||||
|
# Create index on stage_id for better performance
|
||||||
|
await session.execute(text("""
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_todo_stages_stage_id
|
||||||
|
ON todo_stages(stage_id)
|
||||||
|
"""))
|
||||||
|
|
||||||
|
# Populate project_id for existing records
|
||||||
|
await session.execute(text("""
|
||||||
|
UPDATE todo_stages
|
||||||
|
SET project_id = (
|
||||||
|
SELECT tb.project_id
|
||||||
|
FROM todo_boards tb
|
||||||
|
WHERE tb.id = todo_stages.todo_board_id
|
||||||
|
)
|
||||||
|
WHERE project_id IS NULL
|
||||||
|
"""))
|
||||||
|
|
||||||
|
await session.commit()
|
||||||
|
logger.info("Migration 003 completed successfully")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
await session.rollback()
|
||||||
|
logger.error(f"Migration 003 failed: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def _migration_003_down(self, session: AsyncSession):
|
||||||
|
"""Remove project_id column from todo_stages table"""
|
||||||
|
logger.info("Rolling back migration 003: Removing project_id from todo_stages")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Drop indexes first
|
||||||
|
await session.execute(text("DROP INDEX IF EXISTS idx_todo_stages_project_id"))
|
||||||
|
await session.execute(text("DROP INDEX IF EXISTS idx_todo_stages_stage_id"))
|
||||||
|
|
||||||
|
# Remove project_id column (SQLite doesn't support DROP COLUMN directly)
|
||||||
|
# We need to recreate the table without the column
|
||||||
|
await session.execute(text("""
|
||||||
|
CREATE TABLE todo_stages_backup AS
|
||||||
|
SELECT id, todo_board_id, stage_id, stage_index, title, description,
|
||||||
|
status, progress, result, created_at, updated_at
|
||||||
|
FROM todo_stages
|
||||||
|
"""))
|
||||||
|
|
||||||
|
await session.execute(text("DROP TABLE todo_stages"))
|
||||||
|
|
||||||
|
await session.execute(text("""
|
||||||
|
CREATE TABLE todo_stages (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
todo_board_id INTEGER NOT NULL,
|
||||||
|
stage_id VARCHAR(100) NOT NULL,
|
||||||
|
stage_index INTEGER NOT NULL,
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT NOT NULL,
|
||||||
|
status VARCHAR(50) DEFAULT 'pending',
|
||||||
|
progress FLOAT DEFAULT 0.0,
|
||||||
|
result JSON,
|
||||||
|
created_at FLOAT,
|
||||||
|
updated_at FLOAT,
|
||||||
|
FOREIGN KEY (todo_board_id) REFERENCES todo_boards(id)
|
||||||
|
)
|
||||||
|
"""))
|
||||||
|
|
||||||
|
await session.execute(text("""
|
||||||
|
INSERT INTO todo_stages
|
||||||
|
SELECT * FROM todo_stages_backup
|
||||||
|
"""))
|
||||||
|
|
||||||
|
await session.execute(text("DROP TABLE todo_stages_backup"))
|
||||||
|
|
||||||
|
await session.commit()
|
||||||
|
logger.info("Migration 003 rollback completed")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
await session.rollback()
|
||||||
|
logger.error(f"Migration 003 rollback failed: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def _migration_004_up(self, session: AsyncSession):
|
||||||
|
"""Migration 004: Add PPT templates table and update slide_data table"""
|
||||||
|
try:
|
||||||
|
logger.info("Running migration 004: Adding PPT templates table")
|
||||||
|
|
||||||
|
# Create ppt_templates table
|
||||||
|
create_templates_table_sql = """
|
||||||
|
CREATE TABLE IF NOT EXISTS ppt_templates (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
project_id VARCHAR(36) NOT NULL,
|
||||||
|
template_type VARCHAR(50) NOT NULL,
|
||||||
|
template_name VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
html_template TEXT NOT NULL,
|
||||||
|
applicable_scenarios JSON,
|
||||||
|
style_config JSON,
|
||||||
|
usage_count INTEGER DEFAULT 0,
|
||||||
|
created_at FLOAT NOT NULL,
|
||||||
|
updated_at FLOAT NOT NULL,
|
||||||
|
FOREIGN KEY (project_id) REFERENCES projects (project_id)
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
|
await session.execute(text(create_templates_table_sql))
|
||||||
|
|
||||||
|
# Create indexes for ppt_templates
|
||||||
|
await session.execute(text("""
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_ppt_templates_project_id
|
||||||
|
ON ppt_templates (project_id)
|
||||||
|
"""))
|
||||||
|
|
||||||
|
await session.execute(text("""
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_ppt_templates_type
|
||||||
|
ON ppt_templates (template_type)
|
||||||
|
"""))
|
||||||
|
|
||||||
|
# Add template_id column to slide_data table
|
||||||
|
try:
|
||||||
|
await session.execute(text("""
|
||||||
|
ALTER TABLE slide_data
|
||||||
|
ADD COLUMN template_id INTEGER REFERENCES ppt_templates(id)
|
||||||
|
"""))
|
||||||
|
except Exception as e:
|
||||||
|
# Column might already exist, check if it's a duplicate column error
|
||||||
|
if "duplicate column name" not in str(e).lower():
|
||||||
|
raise
|
||||||
|
logger.info("template_id column already exists in slide_data table")
|
||||||
|
|
||||||
|
await session.commit()
|
||||||
|
logger.info("Migration 004 completed successfully")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
await session.rollback()
|
||||||
|
logger.error(f"Migration 004 failed: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def _migration_004_down(self, session: AsyncSession):
|
||||||
|
"""Migration 004 rollback: Remove PPT templates table and template_id column"""
|
||||||
|
try:
|
||||||
|
logger.info("Rolling back migration 004")
|
||||||
|
|
||||||
|
# Drop ppt_templates table
|
||||||
|
await session.execute(text("DROP TABLE IF EXISTS ppt_templates"))
|
||||||
|
|
||||||
|
# Remove template_id column from slide_data table
|
||||||
|
# SQLite doesn't support DROP COLUMN directly, so we need to recreate the table
|
||||||
|
await session.execute(text("""
|
||||||
|
CREATE TABLE slide_data_backup AS
|
||||||
|
SELECT id, project_id, slide_index, slide_id, title, content_type,
|
||||||
|
html_content, slide_metadata, created_at, updated_at
|
||||||
|
FROM slide_data
|
||||||
|
"""))
|
||||||
|
|
||||||
|
await session.execute(text("DROP TABLE slide_data"))
|
||||||
|
|
||||||
|
await session.execute(text("""
|
||||||
|
CREATE TABLE slide_data (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
project_id VARCHAR(36) NOT NULL,
|
||||||
|
slide_index INTEGER NOT NULL,
|
||||||
|
slide_id VARCHAR(100) NOT NULL,
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
content_type VARCHAR(50) NOT NULL,
|
||||||
|
html_content TEXT NOT NULL,
|
||||||
|
slide_metadata JSON,
|
||||||
|
created_at FLOAT NOT NULL,
|
||||||
|
updated_at FLOAT NOT NULL,
|
||||||
|
FOREIGN KEY (project_id) REFERENCES projects (project_id)
|
||||||
|
)
|
||||||
|
"""))
|
||||||
|
|
||||||
|
await session.execute(text("""
|
||||||
|
INSERT INTO slide_data
|
||||||
|
SELECT * FROM slide_data_backup
|
||||||
|
"""))
|
||||||
|
|
||||||
|
await session.execute(text("DROP TABLE slide_data_backup"))
|
||||||
|
|
||||||
|
await session.commit()
|
||||||
|
logger.info("Migration 004 rollback completed")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
await session.rollback()
|
||||||
|
logger.error(f"Migration 004 rollback failed: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def _migration_005_up(self, session: AsyncSession):
|
||||||
|
"""Migration 005: Add project_metadata column to projects table"""
|
||||||
|
try:
|
||||||
|
logger.info("Running migration 005: Adding project_metadata column to projects table")
|
||||||
|
|
||||||
|
# Check if project_metadata column already exists
|
||||||
|
result = await session.execute(text("""
|
||||||
|
PRAGMA table_info(projects)
|
||||||
|
"""))
|
||||||
|
columns = result.fetchall()
|
||||||
|
column_names = [col[1] for col in columns]
|
||||||
|
|
||||||
|
if 'project_metadata' not in column_names:
|
||||||
|
# Add project_metadata column to projects table
|
||||||
|
await session.execute(text("""
|
||||||
|
ALTER TABLE projects
|
||||||
|
ADD COLUMN project_metadata JSON
|
||||||
|
"""))
|
||||||
|
logger.info("Added project_metadata column to projects table")
|
||||||
|
else:
|
||||||
|
logger.info("project_metadata column already exists in projects table")
|
||||||
|
|
||||||
|
await session.commit()
|
||||||
|
logger.info("Migration 005 completed successfully")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
await session.rollback()
|
||||||
|
logger.error(f"Migration 005 failed: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def _migration_005_down(self, session: AsyncSession):
|
||||||
|
"""Migration 005 rollback: Remove project_metadata column from projects table"""
|
||||||
|
try:
|
||||||
|
logger.info("Rolling back migration 005: Removing project_metadata column from projects table")
|
||||||
|
|
||||||
|
# SQLite doesn't support DROP COLUMN directly, so we need to recreate the table
|
||||||
|
await session.execute(text("""
|
||||||
|
CREATE TABLE projects_backup AS
|
||||||
|
SELECT id, project_id, title, scenario, topic, requirements, status,
|
||||||
|
outline, slides_html, slides_data, confirmed_requirements,
|
||||||
|
version, created_at, updated_at
|
||||||
|
FROM projects
|
||||||
|
"""))
|
||||||
|
|
||||||
|
await session.execute(text("DROP TABLE projects"))
|
||||||
|
|
||||||
|
await session.execute(text("""
|
||||||
|
CREATE TABLE projects (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
project_id VARCHAR(36) UNIQUE NOT NULL,
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
scenario VARCHAR(100) NOT NULL,
|
||||||
|
topic VARCHAR(255) NOT NULL,
|
||||||
|
requirements TEXT,
|
||||||
|
status VARCHAR(50) DEFAULT 'draft',
|
||||||
|
outline JSON,
|
||||||
|
slides_html TEXT,
|
||||||
|
slides_data JSON,
|
||||||
|
confirmed_requirements JSON,
|
||||||
|
version INTEGER DEFAULT 1,
|
||||||
|
created_at FLOAT NOT NULL,
|
||||||
|
updated_at FLOAT NOT NULL
|
||||||
|
)
|
||||||
|
"""))
|
||||||
|
|
||||||
|
await session.execute(text("""
|
||||||
|
INSERT INTO projects
|
||||||
|
SELECT * FROM projects_backup
|
||||||
|
"""))
|
||||||
|
|
||||||
|
await session.execute(text("DROP TABLE projects_backup"))
|
||||||
|
|
||||||
|
# Recreate indexes
|
||||||
|
await session.execute(text("CREATE INDEX IF NOT EXISTS idx_projects_status ON projects(status)"))
|
||||||
|
await session.execute(text("CREATE INDEX IF NOT EXISTS idx_projects_scenario ON projects(scenario)"))
|
||||||
|
await session.execute(text("CREATE INDEX IF NOT EXISTS idx_projects_created_at ON projects(created_at)"))
|
||||||
|
|
||||||
|
await session.commit()
|
||||||
|
logger.info("Migration 005 rollback completed")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
await session.rollback()
|
||||||
|
logger.error(f"Migration 005 rollback failed: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def _migration_006_up(self, session: AsyncSession):
|
||||||
|
"""Migration 006: Add is_user_edited field to slide_data table"""
|
||||||
|
try:
|
||||||
|
logger.info("Running migration 006: Adding is_user_edited field to slide_data table")
|
||||||
|
|
||||||
|
# Check if is_user_edited column already exists
|
||||||
|
result = await session.execute(text("""
|
||||||
|
PRAGMA table_info(slide_data)
|
||||||
|
"""))
|
||||||
|
columns = result.fetchall()
|
||||||
|
column_names = [col[1] for col in columns]
|
||||||
|
|
||||||
|
if 'is_user_edited' not in column_names:
|
||||||
|
# Add is_user_edited column to slide_data table
|
||||||
|
await session.execute(text("""
|
||||||
|
ALTER TABLE slide_data
|
||||||
|
ADD COLUMN is_user_edited BOOLEAN DEFAULT 0 NOT NULL
|
||||||
|
"""))
|
||||||
|
logger.info("Added is_user_edited column to slide_data table")
|
||||||
|
else:
|
||||||
|
logger.info("is_user_edited column already exists in slide_data table")
|
||||||
|
|
||||||
|
await session.commit()
|
||||||
|
logger.info("Migration 006 completed successfully")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
await session.rollback()
|
||||||
|
logger.error(f"Migration 006 failed: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def _migration_006_down(self, session: AsyncSession):
|
||||||
|
"""Migration 006 rollback: Remove is_user_edited field from slide_data table"""
|
||||||
|
try:
|
||||||
|
logger.info("Rolling back migration 006: Removing is_user_edited field from slide_data table")
|
||||||
|
|
||||||
|
# SQLite doesn't support DROP COLUMN directly, so we need to recreate the table
|
||||||
|
await session.execute(text("""
|
||||||
|
CREATE TABLE slide_data_backup AS
|
||||||
|
SELECT id, project_id, slide_index, slide_id, title, content_type,
|
||||||
|
html_content, slide_metadata, template_id, created_at, updated_at
|
||||||
|
FROM slide_data
|
||||||
|
"""))
|
||||||
|
|
||||||
|
await session.execute(text("DROP TABLE slide_data"))
|
||||||
|
|
||||||
|
await session.execute(text("""
|
||||||
|
CREATE TABLE slide_data (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
project_id VARCHAR(36) NOT NULL,
|
||||||
|
slide_index INTEGER NOT NULL,
|
||||||
|
slide_id VARCHAR(100) NOT NULL,
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
content_type VARCHAR(50) NOT NULL,
|
||||||
|
html_content TEXT NOT NULL,
|
||||||
|
slide_metadata JSON,
|
||||||
|
template_id INTEGER REFERENCES ppt_templates(id),
|
||||||
|
created_at FLOAT NOT NULL,
|
||||||
|
updated_at FLOAT NOT NULL,
|
||||||
|
FOREIGN KEY (project_id) REFERENCES projects (project_id)
|
||||||
|
)
|
||||||
|
"""))
|
||||||
|
|
||||||
|
await session.execute(text("""
|
||||||
|
INSERT INTO slide_data
|
||||||
|
SELECT * FROM slide_data_backup
|
||||||
|
"""))
|
||||||
|
|
||||||
|
await session.execute(text("DROP TABLE slide_data_backup"))
|
||||||
|
|
||||||
|
await session.commit()
|
||||||
|
logger.info("Migration 006 rollback completed")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
await session.rollback()
|
||||||
|
logger.error(f"Migration 006 rollback failed: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def _create_migration_table(self, session: AsyncSession):
|
||||||
|
"""Create migration tracking table"""
|
||||||
|
create_table_sql = """
|
||||||
|
CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||||
|
version VARCHAR(10) PRIMARY KEY,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
applied_at FLOAT NOT NULL,
|
||||||
|
rollback_sql TEXT
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
|
await session.execute(text(create_table_sql))
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
async def _get_applied_migrations(self, session: AsyncSession) -> List[str]:
|
||||||
|
"""Get list of applied migration versions"""
|
||||||
|
try:
|
||||||
|
result = await session.execute(text("SELECT version FROM schema_migrations ORDER BY version"))
|
||||||
|
return [row[0] for row in result.fetchall()]
|
||||||
|
except Exception:
|
||||||
|
# Table doesn't exist yet
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def _record_migration(self, session: AsyncSession, migration: Dict[str, Any]):
|
||||||
|
"""Record a migration as applied"""
|
||||||
|
insert_sql = """
|
||||||
|
INSERT INTO schema_migrations (version, name, description, applied_at)
|
||||||
|
VALUES (:version, :name, :description, :applied_at)
|
||||||
|
"""
|
||||||
|
|
||||||
|
await session.execute(text(insert_sql), {
|
||||||
|
"version": migration["version"],
|
||||||
|
"name": migration["name"],
|
||||||
|
"description": migration["description"],
|
||||||
|
"applied_at": time.time()
|
||||||
|
})
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
async def _remove_migration_record(self, session: AsyncSession, version: str):
|
||||||
|
"""Remove migration record"""
|
||||||
|
delete_sql = "DELETE FROM schema_migrations WHERE version = :version"
|
||||||
|
await session.execute(text(delete_sql), {"version": version})
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
async def migrate_up(self, target_version: str = None) -> bool:
|
||||||
|
"""Run migrations up to target version"""
|
||||||
|
try:
|
||||||
|
async with AsyncSessionLocal() as session:
|
||||||
|
# Create migration table if it doesn't exist
|
||||||
|
await self._create_migration_table(session)
|
||||||
|
|
||||||
|
# Get applied migrations
|
||||||
|
applied = await self._get_applied_migrations(session)
|
||||||
|
|
||||||
|
# Find migrations to apply
|
||||||
|
to_apply = []
|
||||||
|
for migration in self.migrations:
|
||||||
|
if migration["version"] not in applied:
|
||||||
|
to_apply.append(migration)
|
||||||
|
if target_version and migration["version"] == target_version:
|
||||||
|
break
|
||||||
|
|
||||||
|
if not to_apply:
|
||||||
|
logger.info("No migrations to apply")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Apply migrations
|
||||||
|
for migration in to_apply:
|
||||||
|
logger.info(f"Applying migration {migration['version']}: {migration['name']}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
await migration["up"](session)
|
||||||
|
await self._record_migration(session, migration)
|
||||||
|
logger.info(f"Migration {migration['version']} applied successfully")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to apply migration {migration['version']}: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
logger.info("All migrations applied successfully")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Migration failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def migrate_down(self, target_version: str) -> bool:
|
||||||
|
"""Rollback migrations down to target version"""
|
||||||
|
try:
|
||||||
|
async with AsyncSessionLocal() as session:
|
||||||
|
# Get applied migrations
|
||||||
|
applied = await self._get_applied_migrations(session)
|
||||||
|
|
||||||
|
# Find migrations to rollback
|
||||||
|
to_rollback = []
|
||||||
|
for migration in reversed(self.migrations):
|
||||||
|
if migration["version"] in applied and migration["version"] > target_version:
|
||||||
|
to_rollback.append(migration)
|
||||||
|
|
||||||
|
if not to_rollback:
|
||||||
|
logger.info("No migrations to rollback")
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Rollback migrations
|
||||||
|
for migration in to_rollback:
|
||||||
|
logger.info(f"Rolling back migration {migration['version']}: {migration['name']}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
await migration["down"](session)
|
||||||
|
await self._remove_migration_record(session, migration["version"])
|
||||||
|
logger.info(f"Migration {migration['version']} rolled back successfully")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to rollback migration {migration['version']}: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
logger.info("Migrations rolled back successfully")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Migration rollback failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def get_migration_status(self) -> Dict[str, Any]:
|
||||||
|
"""Get current migration status"""
|
||||||
|
try:
|
||||||
|
async with AsyncSessionLocal() as session:
|
||||||
|
await self._create_migration_table(session)
|
||||||
|
applied = await self._get_applied_migrations(session)
|
||||||
|
|
||||||
|
status = {
|
||||||
|
"current_version": applied[-1] if applied else None,
|
||||||
|
"applied_migrations": applied,
|
||||||
|
"available_migrations": [m["version"] for m in self.migrations],
|
||||||
|
"pending_migrations": [m["version"] for m in self.migrations if m["version"] not in applied]
|
||||||
|
}
|
||||||
|
|
||||||
|
return status
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get migration status: {e}")
|
||||||
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
# Global migration manager instance
|
||||||
|
migration_manager = DatabaseMigration()
|
||||||
Reference in New Issue
Block a user