
- 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
324 lines
11 KiB
Python
Executable file
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() |