church-api/migrate_hymnal_data.py
Benjamin Slingo da06dae89d NUCLEAR: Delete db_operations.rs entirely
- Remove entire utils/db_operations.rs file and all *Operations patterns
- Comment out imports to dead operations
- This breaks compilation temporarily but eliminates the maintenance nightmare
- Next: rewrite db:: modules to use direct SQL instead of operations
- Goal: Clean Handler → Service → Database pattern only
2025-08-28 21:36:14 -04:00

324 lines
11 KiB
Python
Executable file

#!/usr/bin/env python3
"""
Migrate SDA Hymnal data from SQLite to PostgreSQL
This script transfers hymns, thematic lists, and responsive readings
while preserving the original formatting and verse structure.
"""
import sqlite3
import psycopg2
import os
import sys
from typing import Dict, List, Tuple
import logging
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
def get_postgres_connection():
"""Get PostgreSQL database connection from environment variables"""
try:
database_url = os.getenv('DATABASE_URL')
if not database_url:
raise ValueError("DATABASE_URL environment variable not set")
conn = psycopg2.connect(database_url)
return conn
except Exception as e:
logger.error(f"Failed to connect to PostgreSQL: {e}")
sys.exit(1)
def get_sqlite_connection(sqlite_path: str):
"""Get SQLite database connection"""
try:
conn = sqlite3.connect(sqlite_path)
conn.row_factory = sqlite3.Row # Enable column access by name
return conn
except Exception as e:
logger.error(f"Failed to connect to SQLite database at {sqlite_path}: {e}")
sys.exit(1)
def format_old_hymnal_content(content: str) -> str:
"""
Convert 1941 hymnal content to match 1985 format by adding verse numbers.
Assumes verses are separated by double newlines.
"""
if not content or not content.strip():
return content
# Split content by double newlines (verse separators)
verses = content.strip().split('\n\n')
# Filter out empty verses
verses = [verse.strip() for verse in verses if verse.strip()]
# Add verse numbers
formatted_verses = []
for i, verse in enumerate(verses, 1):
# Don't add numbers to very short content (likely chorus or single line)
if len(verse.split('\n')) >= 2:
formatted_verse = f"{i}.\n{verse}"
else:
formatted_verse = verse
formatted_verses.append(formatted_verse)
# Rejoin with double newlines
return '\n\n'.join(formatted_verses)
def get_hymnal_mappings(pg_cursor) -> Dict[str, str]:
"""Get the hymnal ID mappings from PostgreSQL"""
pg_cursor.execute("SELECT id, code FROM hymnals")
mappings = {}
for row in pg_cursor.fetchall():
hymnal_id, code = row
if code == 'sda-1985':
mappings['en-newVersion'] = hymnal_id
elif code == 'sda-1941':
mappings['en-oldVersion'] = hymnal_id
if len(mappings) != 2:
raise ValueError("Could not find both hymnal versions in database")
return mappings
def migrate_hymns(sqlite_conn, pg_conn, hymnal_mappings: Dict[str, str]):
"""Migrate hymns from SQLite to PostgreSQL"""
logger.info("Starting hymns migration...")
sqlite_cursor = sqlite_conn.cursor()
pg_cursor = pg_conn.cursor()
# Get all hymns from SQLite
sqlite_cursor.execute("""
SELECT number, title, content, hymnal_type,
COALESCE(is_favorite, 0) as is_favorite
FROM hymns
ORDER BY hymnal_type, number
""")
hymns = sqlite_cursor.fetchall()
logger.info(f"Found {len(hymns)} hymns to migrate")
# Insert hymns into PostgreSQL
insert_count = 0
for hymn in hymns:
try:
hymnal_id = hymnal_mappings[hymn['hymnal_type']]
# Format 1941 hymnal content to match 1985 format
content = hymn['content']
if hymn['hymnal_type'] == 'en-oldVersion':
content = format_old_hymnal_content(content)
logger.debug(f"Formatted hymn {hymn['number']} from 1941 hymnal")
pg_cursor.execute("""
INSERT INTO hymns (hymnal_id, number, title, content, is_favorite)
VALUES (%s, %s, %s, %s, %s)
ON CONFLICT (hymnal_id, number) DO UPDATE SET
title = EXCLUDED.title,
content = EXCLUDED.content,
is_favorite = EXCLUDED.is_favorite,
updated_at = NOW()
""", (
hymnal_id,
hymn['number'],
hymn['title'],
content,
bool(hymn['is_favorite'])
))
insert_count += 1
if insert_count % 100 == 0:
logger.info(f"Migrated {insert_count} hymns...")
pg_conn.commit()
except Exception as e:
logger.error(f"Failed to migrate hymn {hymn['number']} ({hymn['hymnal_type']}): {e}")
continue
pg_conn.commit()
logger.info(f"Successfully migrated {insert_count} hymns")
def migrate_thematic_lists(sqlite_conn, pg_conn, hymnal_mappings: Dict[str, str]):
"""Migrate thematic lists and ambits from SQLite to PostgreSQL"""
logger.info("Starting thematic lists migration...")
sqlite_cursor = sqlite_conn.cursor()
pg_cursor = pg_conn.cursor()
# Get all thematic lists
sqlite_cursor.execute("""
SELECT id, thematic, hymnal_type
FROM thematic_lists
ORDER BY hymnal_type, id
""")
thematic_lists = sqlite_cursor.fetchall()
logger.info(f"Found {len(thematic_lists)} thematic lists to migrate")
# Track old_id -> new_id mapping for thematic lists
thematic_list_mappings = {}
for idx, theme_list in enumerate(thematic_lists):
try:
hymnal_id = hymnal_mappings[theme_list['hymnal_type']]
# Insert thematic list
pg_cursor.execute("""
INSERT INTO thematic_lists (hymnal_id, name, sort_order)
VALUES (%s, %s, %s)
RETURNING id
""", (hymnal_id, theme_list['thematic'], idx + 1))
new_list_id = pg_cursor.fetchone()[0]
thematic_list_mappings[theme_list['id']] = new_list_id
except Exception as e:
logger.error(f"Failed to migrate thematic list {theme_list['thematic']}: {e}")
continue
pg_conn.commit()
logger.info(f"Successfully migrated {len(thematic_list_mappings)} thematic lists")
# Now migrate thematic ambits
logger.info("Starting thematic ambits migration...")
sqlite_cursor.execute("""
SELECT thematic_list_id, ambit, start_number, end_number
FROM thematic_ambits
ORDER BY thematic_list_id, start_number
""")
ambits = sqlite_cursor.fetchall()
logger.info(f"Found {len(ambits)} thematic ambits to migrate")
ambit_count = 0
for ambit in ambits:
try:
if ambit['thematic_list_id'] not in thematic_list_mappings:
logger.warning(f"Skipping ambit for missing thematic list ID {ambit['thematic_list_id']}")
continue
new_list_id = thematic_list_mappings[ambit['thematic_list_id']]
pg_cursor.execute("""
INSERT INTO thematic_ambits (thematic_list_id, name, start_number, end_number, sort_order)
VALUES (%s, %s, %s, %s, %s)
""", (
new_list_id,
ambit['ambit'],
ambit['start_number'],
ambit['end_number'],
ambit_count + 1
))
ambit_count += 1
except Exception as e:
logger.error(f"Failed to migrate ambit {ambit['ambit']}: {e}")
continue
pg_conn.commit()
logger.info(f"Successfully migrated {ambit_count} thematic ambits")
def migrate_responsive_readings(sqlite_conn, pg_conn):
"""Migrate responsive readings from SQLite to PostgreSQL"""
logger.info("Starting responsive readings migration...")
sqlite_cursor = sqlite_conn.cursor()
pg_cursor = pg_conn.cursor()
# Get all responsive readings
sqlite_cursor.execute("""
SELECT number, title, content, COALESCE(is_favorite, 0) as is_favorite
FROM responsive_readings
ORDER BY number
""")
readings = sqlite_cursor.fetchall()
logger.info(f"Found {len(readings)} responsive readings to migrate")
reading_count = 0
for reading in readings:
try:
pg_cursor.execute("""
INSERT INTO responsive_readings (number, title, content, is_favorite)
VALUES (%s, %s, %s, %s)
ON CONFLICT (number) DO UPDATE SET
title = EXCLUDED.title,
content = EXCLUDED.content,
is_favorite = EXCLUDED.is_favorite,
updated_at = NOW()
""", (
reading['number'],
reading['title'],
reading['content'],
bool(reading['is_favorite'])
))
reading_count += 1
except Exception as e:
logger.error(f"Failed to migrate responsive reading {reading['number']}: {e}")
continue
pg_conn.commit()
logger.info(f"Successfully migrated {reading_count} responsive readings")
def main():
"""Main migration function"""
if len(sys.argv) != 2:
print("Usage: python3 migrate_hymnal_data.py <path_to_hymnarium.db>")
sys.exit(1)
sqlite_path = sys.argv[1]
if not os.path.exists(sqlite_path):
logger.error(f"SQLite database file not found: {sqlite_path}")
sys.exit(1)
logger.info("Starting SDA Hymnal migration...")
logger.info(f"Source: {sqlite_path}")
logger.info(f"Target: PostgreSQL (DATABASE_URL)")
# Connect to both databases
sqlite_conn = get_sqlite_connection(sqlite_path)
pg_conn = get_postgres_connection()
try:
# Get hymnal mappings
pg_cursor = pg_conn.cursor()
hymnal_mappings = get_hymnal_mappings(pg_cursor)
logger.info(f"Found hymnal mappings: {hymnal_mappings}")
# Run migrations
migrate_hymns(sqlite_conn, pg_conn, hymnal_mappings)
migrate_thematic_lists(sqlite_conn, pg_conn, hymnal_mappings)
migrate_responsive_readings(sqlite_conn, pg_conn)
# Print summary
pg_cursor.execute("SELECT COUNT(*) FROM hymns")
total_hymns = pg_cursor.fetchone()[0]
pg_cursor.execute("SELECT COUNT(*) FROM thematic_lists")
total_themes = pg_cursor.fetchone()[0]
pg_cursor.execute("SELECT COUNT(*) FROM responsive_readings")
total_readings = pg_cursor.fetchone()[0]
logger.info("Migration completed successfully!")
logger.info(f"Final counts: {total_hymns} hymns, {total_themes} themes, {total_readings} readings")
except Exception as e:
logger.error(f"Migration failed: {e}")
pg_conn.rollback()
raise
finally:
sqlite_conn.close()
pg_conn.close()
if __name__ == "__main__":
main()