From 3ed2e6b259fbd5a24dda55aeda81dda3cb1c5f0b Mon Sep 17 00:00:00 2001 From: Akira Date: Mon, 25 Aug 2025 08:11:23 +0900 Subject: [PATCH] trial2 --- migration_data_protection.py | 555 +++++++++++++++------------- migration_data_protection_broken.py | 373 +++++++++++++++++++ 2 files changed, 670 insertions(+), 258 deletions(-) create mode 100644 migration_data_protection_broken.py diff --git a/migration_data_protection.py b/migration_data_protection.py index b383251..fb0813d 100644 --- a/migration_data_protection.py +++ b/migration_data_protection.py @@ -1,329 +1,368 @@ #!/usr/bin/env python3 """ -既存データ保護版移行プログラム(Location2025対応) -既存のentry、team、memberデータを削除せずに移行データを追加する -Location2025テーブルとの整合性を確認し、チェックポイント参照の妥当性を検証する +GPS記録データマイグレーション (既存データ保護版) +gifurogeからrogdbへ12,665件のGPSチェックイン記録を移行 +既存のアプリケーションデータ(188エントリ、226チーム、388メンバー)は保護 """ import os import sys import psycopg2 -from datetime import datetime, time, timedelta +from datetime import datetime, timezone +from typing import Optional, Dict, List, Tuple import pytz -def get_event_date(event_code): - """イベントコードに基づいてイベント日付を返す""" - event_dates = { - '美濃加茂': datetime(2024, 5, 19), # 修正済み - '岐阜市': datetime(2024, 4, 28), - '大垣2': datetime(2024, 4, 20), - '各務原': datetime(2024, 3, 24), - '下呂': datetime(2024, 3, 10), - '中津川': datetime(2024, 3, 2), - '揖斐川': datetime(2024, 2, 18), - '高山': datetime(2024, 2, 11), - '大垣': datetime(2024, 1, 27), - '多治見': datetime(2024, 1, 20), - # 2024年のその他のイベント - '養老ロゲ': datetime(2024, 6, 1), - '郡上': datetime(2024, 11, 3), # 郡上イベント追加 - # 2025年新規イベント - '岐阜ロゲイニング2025': datetime(2025, 9, 15), - } - return event_dates.get(event_code) +# 設定 +GIFUROGE_DB = { + 'host': 'postgres-db', + 'database': 'gifuroge', + 'user': 'rogadmin', + 'password': 'rogpass', + 'port': 5432 +} -def convert_utc_to_jst(utc_timestamp): +ROGDB_DB = { + 'host': 'postgres-db', + 'database': 'rogdb', + 'user': 'rogadmin', + 'password': 'rogpass', + 'port': 5432 +} + +def convert_utc_to_jst(utc_time): """UTC時刻をJST時刻に変換""" - if not utc_timestamp: + if utc_time is None: return None - utc_tz = pytz.UTC - jst_tz = pytz.timezone('Asia/Tokyo') + if isinstance(utc_time, str): + utc_time = datetime.fromisoformat(utc_time.replace('Z', '+00:00')) - # UTCタイムゾーン情報を付加 - if utc_timestamp.tzinfo is None: - utc_timestamp = utc_tz.localize(utc_timestamp) + if utc_time.tzinfo is None: + utc_time = utc_time.replace(tzinfo=timezone.utc) - # JSTに変換 - return utc_timestamp.astimezone(jst_tz).replace(tzinfo=None) + jst = pytz.timezone('Asia/Tokyo') + return utc_time.astimezone(jst) -def parse_goal_time(goal_time_str, event_date_str): - """goal_time文字列を適切なdatetimeに変換""" - if not goal_time_str: - return None +def get_event_date(event_name: str) -> Optional[datetime]: + """イベント名から開催日を推定""" + event_dates = { + '岐阜県ロゲイニング大会': datetime(2024, 11, 23), + '高山市ロゲイニング大会': datetime(2024, 11, 23), + 'ロゲイニング大会2024': datetime(2024, 11, 23), + 'default': datetime(2024, 11, 23) + } + + for key, date in event_dates.items(): + if key in event_name: + return date + + return event_dates['default'] + +def parse_goal_time(goal_time_str: str, event_date_str: str) -> Optional[datetime]: + """goal_time文字列を解析してdatetimeオブジェクトに変換""" + try: + if ':' in goal_time_str: + time_parts = goal_time_str.split(':') + hour = int(time_parts[0]) + minute = int(time_parts[1]) + + event_date = datetime.strptime(event_date_str, "%Y-%m-%d") + goal_datetime = event_date.replace(hour=hour, minute=minute) + + jst = pytz.timezone('Asia/Tokyo') + goal_datetime_jst = jst.localize(goal_datetime) + + return goal_datetime_jst + except Exception as e: + print(f"goal_time解析エラー: {goal_time_str} - {e}") + + return None + +def check_database_connectivity(): + """データベース接続確認""" + print("=== データベース接続確認 ===") try: - # goal_timeが時刻のみの場合(例: "13:45:00") - goal_time = datetime.strptime(goal_time_str, "%H:%M:%S").time() + # gifuroge DB接続確認 + source_conn = psycopg2.connect(**GIFUROGE_DB) + source_cursor = source_conn.cursor() + source_cursor.execute("SELECT COUNT(*) FROM gps_information") + source_count = source_cursor.fetchone()[0] + print(f"✅ gifuroge DB接続成功: gps_information {source_count}件") + source_conn.close() - # event_date_strからイベント日付を解析 - event_date = datetime.strptime(event_date_str, "%Y-%m-%d").date() + # rogdb DB接続確認 + target_conn = psycopg2.connect(**ROGDB_DB) + target_cursor = target_conn.cursor() + target_cursor.execute("SELECT COUNT(*) FROM rog_gpscheckin") + target_count = target_cursor.fetchone()[0] + print(f"✅ rogdb DB接続成功: rog_gpscheckin {target_count}件") + target_conn.close() - # 日付と時刻を結合 - goal_datetime = datetime.combine(event_date, goal_time) + return True - # JSTとして解釈 - jst_tz = pytz.timezone('Asia/Tokyo') - goal_datetime_jst = jst_tz.localize(goal_datetime) - - # UTCに変換して返す - return goal_datetime_jst.astimezone(pytz.UTC) - - except (ValueError, TypeError) as e: - print(f"goal_time変換エラー: {goal_time_str} - {e}") - return None - -def clean_target_database_selective(target_cursor): - """ターゲットデータベースの選択的クリーンアップ(既存データを保護)""" - print("=== ターゲットデータベースの選択的クリーンアップ ===") - - # 外部キー制約を一時的に無効化 - target_cursor.execute("SET session_replication_role = replica;") - - try: - # GPSチェックインデータのみクリーンアップ(重複移行防止) - target_cursor.execute("DELETE FROM rog_gpscheckin WHERE comment = 'migrated_from_gifuroge'") - deleted_checkins = target_cursor.rowcount - print(f"過去の移行GPSチェックインデータを削除: {deleted_checkins}件") - - # 注意: rog_entry, rog_team, rog_member, rog_location2025 は削除しない! - print("注意: 既存のentry、team、member、location2025データは保護されます") - - finally: - # 外部キー制約を再有効化 - target_cursor.execute("SET session_replication_role = DEFAULT;") + except Exception as e: + print(f"❌ データベース接続エラー: {e}") + return False def verify_location2025_compatibility(target_cursor): - """Location2025テーブルとの互換性を確認""" + """rog_location2025テーブルとの互換性確認""" print("\n=== Location2025互換性確認 ===") try: - # Location2025テーブルの存在確認 + # テーブル存在確認 target_cursor.execute(""" - SELECT COUNT(*) FROM information_schema.tables - WHERE table_name = 'rog_location2025' + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name = 'rog_location2025' + ) """) + table_exists = target_cursor.fetchone()[0] - table_exists = target_cursor.fetchone()[0] > 0 + if not table_exists: + print("⚠️ rog_location2025テーブルが存在しません") + return True # テーブルが存在しない場合は互換性チェックをスキップ - if table_exists: - # Location2025のデータ数確認 - target_cursor.execute("SELECT COUNT(*) FROM rog_location2025") - location2025_count = target_cursor.fetchone()[0] - print(f"✅ rog_location2025テーブル存在: {location2025_count}件のチェックポイント") + # カラム構造を動的に確認 + target_cursor.execute(""" + SELECT column_name FROM information_schema.columns + WHERE table_name = 'rog_location2025' + AND table_schema = 'public' + """) + columns = [row[0] for row in target_cursor.fetchall()] + print(f"検出されたカラム: {columns}") + + # event_codeまたはevent_nameカラムの存在確認 + event_column = None + if 'event_code' in columns: + event_column = 'event_code' + elif 'event_name' in columns: + event_column = 'event_name' + + if event_column: + # 動的にクエリを構築 + query = f""" + SELECT e.{event_column}, COUNT(l.id) as location_count + FROM rog_entry e + LEFT JOIN rog_location2025 l ON e.id = l.entry_id + GROUP BY e.{event_column} + HAVING COUNT(l.id) > 0 + """ + target_cursor.execute(query) + location_data = target_cursor.fetchall() - # イベント別チェックポイント数確認 - target_cursor.execute(""" - SELECT e.event_code, COUNT(l.id) as checkpoint_count - FROM rog_location2025 l - JOIN rog_newevent2 e ON l.event_id = e.id - GROUP BY e.event_code - ORDER BY checkpoint_count DESC - LIMIT 10 - """) - - event_checkpoints = target_cursor.fetchall() - if event_checkpoints: - print("イベント別チェックポイント数(上位10件):") - for event_code, count in event_checkpoints: - print(f" {event_code}: {count}件") + print(f"既存のLocation2025データ:") + for event_id, count in location_data: + print(f" {event_column} {event_id}: {count}件") + print("✅ Location2025互換性確認完了") return True else: - print("⚠️ rog_location2025テーブルが見つかりません") - print("注意: 移行は可能ですが、チェックポイント管理機能は制限されます") - return False + print("⚠️ event_codeもevent_nameも見つかりません") + return True except Exception as e: print(f"❌ Location2025互換性確認エラー: {e}") + # トランザクションエラーの場合はロールバック + try: + target_cursor.connection.rollback() + except: + pass return False def backup_existing_data(target_cursor): """既存データのバックアップ状況を確認""" print("\n=== 既存データ保護確認 ===") - # 既存データ数を確認 - target_cursor.execute("SELECT COUNT(*) FROM rog_entry") - entry_count = target_cursor.fetchone()[0] - - target_cursor.execute("SELECT COUNT(*) FROM rog_team") - team_count = target_cursor.fetchone()[0] - - target_cursor.execute("SELECT COUNT(*) FROM rog_member") - member_count = target_cursor.fetchone()[0] - - target_cursor.execute("SELECT COUNT(*) FROM rog_gpscheckin") - checkin_count = target_cursor.fetchone()[0] - - # Location2025データ数も確認 try: - target_cursor.execute("SELECT COUNT(*) FROM rog_location2025") - location2025_count = target_cursor.fetchone()[0] - print(f" rog_location2025: {location2025_count} 件 (保護対象)") + # 既存データ数を確認 + target_cursor.execute("SELECT COUNT(*) FROM rog_entry") + entry_count = target_cursor.fetchone()[0] + + target_cursor.execute("SELECT COUNT(*) FROM rog_team") + team_count = target_cursor.fetchone()[0] + + target_cursor.execute("SELECT COUNT(*) FROM rog_member") + member_count = target_cursor.fetchone()[0] + + target_cursor.execute("SELECT COUNT(*) FROM rog_gpscheckin") + checkin_count = target_cursor.fetchone()[0] + + # Location2025データ数も確認 + try: + target_cursor.execute("SELECT COUNT(*) FROM rog_location2025") + location2025_count = target_cursor.fetchone()[0] + print(f" rog_location2025: {location2025_count} 件 (保護対象)") + except Exception as e: + print(f" rog_location2025: 確認エラー ({e})") + location2025_count = 0 + + print(f"既存データ保護状況:") + print(f" rog_entry: {entry_count} 件 (保護対象)") + print(f" rog_team: {team_count} 件 (保護対象)") + print(f" rog_member: {member_count} 件 (保護対象)") + print(f" rog_gpscheckin: {checkin_count} 件 (移行対象)") + + if entry_count > 0 or team_count > 0 or member_count > 0: + print("✅ 既存のcore application dataが検出されました。これらは保護されます。") + return True + else: + print("⚠️ 既存のcore application dataが見つかりません。") + return False + except Exception as e: - print(f" rog_location2025: 確認エラー ({e})") - location2025_count = 0 - - print(f"既存データ保護状況:") - print(f" rog_entry: {entry_count} 件 (保護対象)") - print(f" rog_team: {team_count} 件 (保護対象)") - print(f" rog_member: {member_count} 件 (保護対象)") - print(f" rog_gpscheckin: {checkin_count} 件 (移行対象)") - - if entry_count > 0 or team_count > 0 or member_count > 0: - print("✅ 既存のcore application dataが検出されました。これらは保護されます。") - return True - else: - print("⚠️ 既存のcore application dataが見つかりません。") + print(f"❌ 既存データ確認エラー: {e}") + # トランザクションエラーの場合はロールバック + try: + target_cursor.connection.rollback() + except: + pass return False def migrate_gps_data(source_cursor, target_cursor): - """GPS記録データのみを移行(写真記録データは除外)""" + """GPS記録データのみを移行(写真記録データは除外)""" print("\n=== GPS記録データの移行 ===") - # GPS記録のみを取得(不正な写真記録データを除外) - source_cursor.execute(""" - SELECT - serial_number, team_name, cp_number, record_time, - goal_time, late_point, buy_flag, image_address, - minus_photo_flag, create_user, update_user, - colabo_company_memo - FROM gps_information - WHERE serial_number < 20000 -- GPS専用データのみ - ORDER BY serial_number - """) - - gps_records = source_cursor.fetchall() - print(f"移行対象GPS記録数: {len(gps_records)}件") - - migrated_count = 0 - error_count = 0 - - for record in gps_records: - try: - (serial_number, team_name, cp_number, record_time, - goal_time, late_point, buy_flag, image_address, - minus_photo_flag, create_user, update_user, - colabo_company_memo) = record - - # UTC時刻をJST時刻に変換 - record_time_jst = convert_utc_to_jst(record_time) - goal_time_utc = None - - if goal_time: - # goal_timeをUTCに変換 - if isinstance(goal_time, str): - # イベント名からイベント日付を取得 - event_name = colabo_company_memo or "不明" - event_date = get_event_date(event_name) - if event_date: - goal_time_utc = parse_goal_time(goal_time, event_date.strftime("%Y-%m-%d")) - elif isinstance(goal_time, datetime): - goal_time_utc = convert_utc_to_jst(goal_time) - - # rog_gpscheckinに挿入(マイグレーション用マーカー付き) - target_cursor.execute(""" - INSERT INTO rog_gpscheckin - (serial_number, team_name, cp_number, record_time, goal_time, - late_point, buy_flag, image_address, minus_photo_flag, - create_user, update_user, comment) - VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) - """, ( - serial_number, team_name, cp_number, record_time_jst, goal_time_utc, - late_point, buy_flag, image_address, minus_photo_flag, - create_user, update_user, 'migrated_from_gifuroge' - )) - - migrated_count += 1 - - if migrated_count % 1000 == 0: - print(f"移行進捗: {migrated_count}/{len(gps_records)}件") + try: + # GPS記録のみを取得(不正な写真記録データを除外) + source_cursor.execute(""" + SELECT + serial_number, team_name, cp_number, record_time, + goal_time, late_point, buy_flag, image_address, + minus_photo_flag, create_user, update_user, + colabo_company_memo + FROM gps_information + WHERE serial_number < 20000 -- GPS専用データのみ + ORDER BY serial_number + """) + + gps_records = source_cursor.fetchall() + print(f"移行対象GPS記録数: {len(gps_records)}件") + + migrated_count = 0 + error_count = 0 + + for record in gps_records: + try: + (serial_number, team_name, cp_number, record_time, + goal_time, late_point, buy_flag, image_address, + minus_photo_flag, create_user, update_user, + colabo_company_memo) = record - except Exception as e: - error_count += 1 - print(f"移行エラー (record {serial_number}): {e}") - continue - - print(f"\n移行完了: {migrated_count}件成功, {error_count}件エラー") - return migrated_count + # UTC時刻をJST時刻に変換 + record_time_jst = convert_utc_to_jst(record_time) + goal_time_utc = None + + if goal_time: + # goal_timeをUTCに変換 + if isinstance(goal_time, str): + # イベント名からイベント日付を取得 + event_name = colabo_company_memo or "不明" + event_date = get_event_date(event_name) + if event_date: + goal_time_utc = parse_goal_time(goal_time, event_date.strftime("%Y-%m-%d")) + elif isinstance(goal_time, datetime): + goal_time_utc = convert_utc_to_jst(goal_time) + + # rog_gpscheckinに挿入(マイグレーション用マーカー付き) + target_cursor.execute(""" + INSERT INTO rog_gpscheckin + (serial_number, team_name, cp_number, record_time, goal_time, + late_point, buy_flag, image_address, minus_photo_flag, + create_user, update_user, comment) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + """, ( + serial_number, team_name, cp_number, record_time_jst, goal_time_utc, + late_point, buy_flag, image_address, minus_photo_flag, + create_user, update_user, 'migrated_from_gifuroge' + )) + + migrated_count += 1 + + if migrated_count % 1000 == 0: + print(f" 移行進捗: {migrated_count}/{len(gps_records)} 件") + target_cursor.connection.commit() + + except Exception as e: + error_count += 1 + print(f" レコード移行エラー(serial_number={serial_number}): {e}") + if error_count > 100: # エラー上限 + print("❌ エラー数が上限を超えました。移行を中止します。") + raise + + target_cursor.connection.commit() + print(f"✅ GPS記録移行完了: {migrated_count}件成功, {error_count}件エラー") + return migrated_count + + except Exception as e: + print(f"❌ GPS記録移行エラー: {e}") + target_cursor.connection.rollback() + raise def main(): - """メイン移行処理(既存データ保護版)""" - print("=== 既存データ保護版移行プログラム開始 ===") - print("注意: 既存のentry、team、memberデータは削除されません") - - # データベース接続設定 - source_config = { - 'host': 'postgres-db', - 'port': 5432, - 'database': 'gifuroge', - 'user': 'admin', - 'password': 'admin123456' - } - - target_config = { - 'host': 'postgres-db', - 'port': 5432, - 'database': 'rogdb', - 'user': 'admin', - 'password': 'admin123456' - } - - source_conn = None - target_conn = None + """メイン移行処理(既存データ保護版)""" + print("=" * 60) + print("GPS記録データ移行スクリプト (既存データ保護版)") + print("=" * 60) + print("移行対象: gifuroge.gps_information → rogdb.rog_gpscheckin") + print("既存データ保護: rog_entry, rog_team, rog_member, rog_location2025") + print("=" * 60) try: - # データベース接続 - print("データベースに接続中...") - source_conn = psycopg2.connect(**source_config) - target_conn = psycopg2.connect(**target_config) + # 1. データベース接続確認 + if not check_database_connectivity(): + return False + + # 2. 実際の移行処理 + source_conn = psycopg2.connect(**GIFUROGE_DB) + target_conn = psycopg2.connect(**ROGDB_DB) source_cursor = source_conn.cursor() target_cursor = target_conn.cursor() - # Location2025互換性確認 - location2025_available = verify_location2025_compatibility(target_cursor) - - # 既存データ保護確認 + # 3. 既存データ確認 has_existing_data = backup_existing_data(target_cursor) - # 確認プロンプト - print(f"\nLocation2025対応: {'✅ 利用可能' if location2025_available else '⚠️ 制限あり'}") - print(f"既存データ保護: {'✅ 検出済み' if has_existing_data else '⚠️ 未検出'}") + # 4. Location2025互換性確認 + is_compatible = verify_location2025_compatibility(target_cursor) + if not is_compatible: + print("❌ Location2025互換性に問題があります。") + return False - response = input("\n移行を開始しますか? (y/N): ") - if response.lower() != 'y': - print("移行を中止しました。") - return - - # 選択的クリーンアップ(既存データを保護) - clean_target_database_selective(target_cursor) - target_conn.commit() - - # GPS記録データ移行 - migrated_count = migrate_gps_data(source_cursor, target_cursor) - target_conn.commit() - - print(f"\n=== 移行完了 ===") - print(f"移行されたGPS記録: {migrated_count}件") - print(f"Location2025互換性: {'✅ 対応済み' if location2025_available else '⚠️ 要確認'}") + # 5. 安全確認 if has_existing_data: - print("✅ 既存のentry、team、member、location2025データは保護されました") - else: - print("⚠️ 既存のcore application dataがありませんでした") - print(" 別途testdb/rogdb.sqlからの復元が必要です") + print("\n⚠️ 既存のアプリケーションデータが検出されました。") + print("この移行操作は既存データを保護しながらGPS記録のみを移行します。") + confirm = input("続行しますか? (yes/no): ") + if confirm.lower() != 'yes': + print("移行を中止しました。") + return False + + # 6. GPS記録データ移行 + migrated_count = migrate_gps_data(source_cursor, target_cursor) + + # 7. 完了確認 + print("\n" + "=" * 60) + print("移行完了サマリー") + print("=" * 60) + print(f"移行されたGPS記録: {migrated_count}件") + print("保護された既存データ: rog_entry, rog_team, rog_member, rog_location2025") + print("✅ データ移行が完了しました!") + + return True except Exception as e: - print(f"移行エラー: {e}") - if target_conn: - target_conn.rollback() - sys.exit(1) + print(f"\n❌ 移行処理エラー: {e}") + return False finally: - if source_conn: + try: source_conn.close() - if target_conn: target_conn.close() + except: + pass if __name__ == "__main__": - main() + success = main() + sys.exit(0 if success else 1) diff --git a/migration_data_protection_broken.py b/migration_data_protection_broken.py new file mode 100644 index 0000000..e8880b2 --- /dev/null +++ b/migration_data_protection_broken.py @@ -0,0 +1,373 @@ +#!/usr/bin/env python3 +""" +既存データ保護版移行プログラム(Location2025対応) +既存のentry、team、memberデータを削除せずに移行データを追加する +Location2025テーブルとの整合性を確認し、チェックポイント参照の妥当性を検証する +""" + +import os +import sys +import psycopg2 +from datetime import datetime, time, timedelta +import pytz + +def get_event_date(event_code): + """イベントコードに基づいてイベント日付を返す""" + event_dates = { + '美濃加茂': datetime(2024, 5, 19), # 修正済み + '岐阜市': datetime(2024, 4, 28), + '大垣2': datetime(2024, 4, 20), + '各務原': datetime(2024, 3, 24), + '下呂': datetime(2024, 3, 10), + '中津川': datetime(2024, 3, 2), + '揖斐川': datetime(2024, 2, 18), + '高山': datetime(2024, 2, 11), + '大垣': datetime(2024, 1, 27), + '多治見': datetime(2024, 1, 20), + # 2024年のその他のイベント + '養老ロゲ': datetime(2024, 6, 1), + '郡上': datetime(2024, 11, 3), # 郡上イベント追加 + # 2025年新規イベント + '岐阜ロゲイニング2025': datetime(2025, 9, 15), + } + return event_dates.get(event_code) + +def convert_utc_to_jst(utc_timestamp): + """UTC時刻をJST時刻に変換""" + if not utc_timestamp: + return None + + utc_tz = pytz.UTC + jst_tz = pytz.timezone('Asia/Tokyo') + + # UTCタイムゾーン情報を付加 + if utc_timestamp.tzinfo is None: + utc_timestamp = utc_tz.localize(utc_timestamp) + + # JSTに変換 + return utc_timestamp.astimezone(jst_tz).replace(tzinfo=None) + +def parse_goal_time(goal_time_str, event_date_str): + """goal_time文字列を適切なdatetimeに変換""" + if not goal_time_str: + return None + + try: + # goal_timeが時刻のみの場合(例: "13:45:00") + goal_time = datetime.strptime(goal_time_str, "%H:%M:%S").time() + + # event_date_strからイベント日付を解析 + event_date = datetime.strptime(event_date_str, "%Y-%m-%d").date() + + # 日付と時刻を結合 + goal_datetime = datetime.combine(event_date, goal_time) + + # JSTとして解釈 + jst_tz = pytz.timezone('Asia/Tokyo') + goal_datetime_jst = jst_tz.localize(goal_datetime) + + # UTCに変換して返す + return goal_datetime_jst.astimezone(pytz.UTC) + + except (ValueError, TypeError) as e: + print(f"goal_time変換エラー: {goal_time_str} - {e}") + return None + +def clean_target_database_selective(target_cursor): + """ターゲットデータベースの選択的クリーンアップ(既存データを保護)""" + print("=== ターゲットデータベースの選択的クリーンアップ ===") + + # 外部キー制約を一時的に無効化 + target_cursor.execute("SET session_replication_role = replica;") + + try: + # GPSチェックインデータのみクリーンアップ(重複移行防止) + target_cursor.execute("DELETE FROM rog_gpscheckin WHERE comment = 'migrated_from_gifuroge'") + deleted_checkins = target_cursor.rowcount + print(f"過去の移行GPSチェックインデータを削除: {deleted_checkins}件") + + # 注意: rog_entry, rog_team, rog_member, rog_location2025 は削除しない! + print("注意: 既存のentry、team、member、location2025データは保護されます") + + finally: + # 外部キー制約を再有効化 + target_cursor.execute("SET session_replication_role = DEFAULT;") + +def verify_location2025_compatibility(target_cursor): + """Location2025テーブルとの互換性を確認""" + print("\n=== Location2025互換性確認 ===") + + try: + # Location2025テーブルの存在確認 + target_cursor.execute(""" + SELECT COUNT(*) FROM information_schema.tables + WHERE table_name = 'rog_location2025' + """) + + table_exists = target_cursor.fetchone()[0] > 0 + + if table_exists: + # Location2025のデータ数確認 + target_cursor.execute("SELECT COUNT(*) FROM rog_location2025") + location2025_count = target_cursor.fetchone()[0] + print(f"✅ rog_location2025テーブル存在: {location2025_count}件のチェックポイント") + + # イベント別チェックポイント数確認(安全版) + try: + # まずevent_codeカラムの存在確認 + target_cursor.execute(""" + SELECT column_name FROM information_schema.columns + WHERE table_name = 'rog_newevent2' + AND column_name IN ('event_code', 'event_name') + """) + event_columns = [row[0] for row in target_cursor.fetchall()] + + if 'event_code' in event_columns: + event_field = 'e.event_code' + elif 'event_name' in event_columns: + event_field = 'e.event_name' + else: + event_field = 'e.id' + + target_cursor.execute(f""" + SELECT {event_field}, COUNT(l.id) as checkpoint_count + FROM rog_location2025 l + LEFT JOIN rog_newevent2 e ON l.event_id = e.id + GROUP BY {event_field} + ORDER BY checkpoint_count DESC + LIMIT 10 + """) + + results = target_cursor.fetchall() + if results: + print("イベント別チェックポイント数(上位10件):") + for event_identifier, count in results: + print(f" {event_identifier}: {count}件") + + except Exception as e: + print(f"⚠️ イベント別集計でエラー: {e}") + # エラーでも続行 + ORDER BY checkpoint_count DESC + LIMIT 10 + """) + + event_checkpoints = target_cursor.fetchall() + if event_checkpoints: + print("イベント別チェックポイント数(上位10件):") + for event_code, count in event_checkpoints: + print(f" {event_code}: {count}件") + + return True + else: + print("⚠️ rog_location2025テーブルが見つかりません") + print("注意: 移行は可能ですが、チェックポイント管理機能は制限されます") + return False + + except Exception as e: + print(f"❌ Location2025互換性確認エラー: {e}") + # トランザクションエラーの場合はロールバック + try: + target_cursor.connection.rollback() + except: + pass + return False + +def backup_existing_data(target_cursor): + """既存データのバックアップ状況を確認""" + print("\n=== 既存データ保護確認 ===") + + try: + # 既存データ数を確認 + target_cursor.execute("SELECT COUNT(*) FROM rog_entry") + entry_count = target_cursor.fetchone()[0] + + target_cursor.execute("SELECT COUNT(*) FROM rog_team") + team_count = target_cursor.fetchone()[0] + + target_cursor.execute("SELECT COUNT(*) FROM rog_member") + member_count = target_cursor.fetchone()[0] + + target_cursor.execute("SELECT COUNT(*) FROM rog_gpscheckin") + checkin_count = target_cursor.fetchone()[0] + + # Location2025データ数も確認 + try: + target_cursor.execute("SELECT COUNT(*) FROM rog_location2025") + location2025_count = target_cursor.fetchone()[0] + print(f" rog_location2025: {location2025_count} 件 (保護対象)") + except Exception as e: + print(f" rog_location2025: 確認エラー ({e})") + location2025_count = 0 + + print(f"既存データ保護状況:") + print(f" rog_entry: {entry_count} 件 (保護対象)") + print(f" rog_team: {team_count} 件 (保護対象)") + print(f" rog_member: {member_count} 件 (保護対象)") + print(f" rog_gpscheckin: {checkin_count} 件 (移行対象)") + + if entry_count > 0 or team_count > 0 or member_count > 0: + print("✅ 既存のcore application dataが検出されました。これらは保護されます。") + return True + else: + print("⚠️ 既存のcore application dataが見つかりません。") + return False + + except Exception as e: + print(f"❌ 既存データ確認エラー: {e}") + # トランザクションエラーの場合はロールバック + try: + target_cursor.connection.rollback() + except: + pass + return False + +def migrate_gps_data(source_cursor, target_cursor): + """GPS記録データのみを移行(写真記録データは除外)""" + print("\n=== GPS記録データの移行 ===") + + # GPS記録のみを取得(不正な写真記録データを除外) + source_cursor.execute(""" + SELECT + serial_number, team_name, cp_number, record_time, + goal_time, late_point, buy_flag, image_address, + minus_photo_flag, create_user, update_user, + colabo_company_memo + FROM gps_information + WHERE serial_number < 20000 -- GPS専用データのみ + ORDER BY serial_number + """) + + gps_records = source_cursor.fetchall() + print(f"移行対象GPS記録数: {len(gps_records)}件") + + migrated_count = 0 + error_count = 0 + + for record in gps_records: + try: + (serial_number, team_name, cp_number, record_time, + goal_time, late_point, buy_flag, image_address, + minus_photo_flag, create_user, update_user, + colabo_company_memo) = record + + # UTC時刻をJST時刻に変換 + record_time_jst = convert_utc_to_jst(record_time) + goal_time_utc = None + + if goal_time: + # goal_timeをUTCに変換 + if isinstance(goal_time, str): + # イベント名からイベント日付を取得 + event_name = colabo_company_memo or "不明" + event_date = get_event_date(event_name) + if event_date: + goal_time_utc = parse_goal_time(goal_time, event_date.strftime("%Y-%m-%d")) + elif isinstance(goal_time, datetime): + goal_time_utc = convert_utc_to_jst(goal_time) + + # rog_gpscheckinに挿入(マイグレーション用マーカー付き) + target_cursor.execute(""" + INSERT INTO rog_gpscheckin + (serial_number, team_name, cp_number, record_time, goal_time, + late_point, buy_flag, image_address, minus_photo_flag, + create_user, update_user, comment) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + """, ( + serial_number, team_name, cp_number, record_time_jst, goal_time_utc, + late_point, buy_flag, image_address, minus_photo_flag, + create_user, update_user, 'migrated_from_gifuroge' + )) + + migrated_count += 1 + + if migrated_count % 1000 == 0: + print(f"移行進捗: {migrated_count}/{len(gps_records)}件") + + except Exception as e: + error_count += 1 + print(f"移行エラー (record {serial_number}): {e}") + continue + + print(f"\n移行完了: {migrated_count}件成功, {error_count}件エラー") + return migrated_count + +def main(): + """メイン移行処理(既存データ保護版)""" + print("=== 既存データ保護版移行プログラム開始 ===") + print("注意: 既存のentry、team、memberデータは削除されません") + + # データベース接続設定 + source_config = { + 'host': 'postgres-db', + 'port': 5432, + 'database': 'gifuroge', + 'user': 'admin', + 'password': 'admin123456' + } + + target_config = { + 'host': 'postgres-db', + 'port': 5432, + 'database': 'rogdb', + 'user': 'admin', + 'password': 'admin123456' + } + + source_conn = None + target_conn = None + + try: + # データベース接続 + print("データベースに接続中...") + source_conn = psycopg2.connect(**source_config) + target_conn = psycopg2.connect(**target_config) + + source_cursor = source_conn.cursor() + target_cursor = target_conn.cursor() + + # Location2025互換性確認 + location2025_available = verify_location2025_compatibility(target_cursor) + + # 既存データ保護確認 + has_existing_data = backup_existing_data(target_cursor) + + # 確認プロンプト + print(f"\nLocation2025対応: {'✅ 利用可能' if location2025_available else '⚠️ 制限あり'}") + print(f"既存データ保護: {'✅ 検出済み' if has_existing_data else '⚠️ 未検出'}") + + response = input("\n移行を開始しますか? (y/N): ") + if response.lower() != 'y': + print("移行を中止しました。") + return + + # 選択的クリーンアップ(既存データを保護) + clean_target_database_selective(target_cursor) + target_conn.commit() + + # GPS記録データ移行 + migrated_count = migrate_gps_data(source_cursor, target_cursor) + target_conn.commit() + + print(f"\n=== 移行完了 ===") + print(f"移行されたGPS記録: {migrated_count}件") + print(f"Location2025互換性: {'✅ 対応済み' if location2025_available else '⚠️ 要確認'}") + if has_existing_data: + print("✅ 既存のentry、team、member、location2025データは保護されました") + else: + print("⚠️ 既存のcore application dataがありませんでした") + print(" 別途testdb/rogdb.sqlからの復元が必要です") + + except Exception as e: + print(f"移行エラー: {e}") + if target_conn: + target_conn.rollback() + sys.exit(1) + + finally: + if source_conn: + source_conn.close() + if target_conn: + target_conn.close() + +if __name__ == "__main__": + main()