trial2
This commit is contained in:
@ -1,147 +1,181 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
既存データ保護版移行プログラム(Location2025対応)
|
GPS記録データマイグレーション (既存データ保護版)
|
||||||
既存のentry、team、memberデータを削除せずに移行データを追加する
|
gifurogeからrogdbへ12,665件のGPSチェックイン記録を移行
|
||||||
Location2025テーブルとの整合性を確認し、チェックポイント参照の妥当性を検証する
|
既存のアプリケーションデータ(188エントリ、226チーム、388メンバー)は保護
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import psycopg2
|
import psycopg2
|
||||||
from datetime import datetime, time, timedelta
|
from datetime import datetime, timezone
|
||||||
|
from typing import Optional, Dict, List, Tuple
|
||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
def get_event_date(event_code):
|
# 設定
|
||||||
"""イベントコードに基づいてイベント日付を返す"""
|
GIFUROGE_DB = {
|
||||||
event_dates = {
|
'host': 'postgres-db',
|
||||||
'美濃加茂': datetime(2024, 5, 19), # 修正済み
|
'database': 'gifuroge',
|
||||||
'岐阜市': datetime(2024, 4, 28),
|
'user': 'rogadmin',
|
||||||
'大垣2': datetime(2024, 4, 20),
|
'password': 'rogpass',
|
||||||
'各務原': datetime(2024, 3, 24),
|
'port': 5432
|
||||||
'下呂': 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):
|
ROGDB_DB = {
|
||||||
|
'host': 'postgres-db',
|
||||||
|
'database': 'rogdb',
|
||||||
|
'user': 'rogadmin',
|
||||||
|
'password': 'rogpass',
|
||||||
|
'port': 5432
|
||||||
|
}
|
||||||
|
|
||||||
|
def convert_utc_to_jst(utc_time):
|
||||||
"""UTC時刻をJST時刻に変換"""
|
"""UTC時刻をJST時刻に変換"""
|
||||||
if not utc_timestamp:
|
if utc_time is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
utc_tz = pytz.UTC
|
if isinstance(utc_time, str):
|
||||||
jst_tz = pytz.timezone('Asia/Tokyo')
|
utc_time = datetime.fromisoformat(utc_time.replace('Z', '+00:00'))
|
||||||
|
|
||||||
# UTCタイムゾーン情報を付加
|
if utc_time.tzinfo is None:
|
||||||
if utc_timestamp.tzinfo is None:
|
utc_time = utc_time.replace(tzinfo=timezone.utc)
|
||||||
utc_timestamp = utc_tz.localize(utc_timestamp)
|
|
||||||
|
|
||||||
# JSTに変換
|
jst = pytz.timezone('Asia/Tokyo')
|
||||||
return utc_timestamp.astimezone(jst_tz).replace(tzinfo=None)
|
return utc_time.astimezone(jst)
|
||||||
|
|
||||||
|
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}")
|
||||||
|
|
||||||
def parse_goal_time(goal_time_str, event_date_str):
|
|
||||||
"""goal_time文字列を適切なdatetimeに変換"""
|
|
||||||
if not goal_time_str:
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def check_database_connectivity():
|
||||||
|
"""データベース接続確認"""
|
||||||
|
print("=== データベース接続確認 ===")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# goal_timeが時刻のみの場合(例: "13:45:00")
|
# gifuroge DB接続確認
|
||||||
goal_time = datetime.strptime(goal_time_str, "%H:%M:%S").time()
|
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からイベント日付を解析
|
# rogdb DB接続確認
|
||||||
event_date = datetime.strptime(event_date_str, "%Y-%m-%d").date()
|
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()
|
||||||
|
|
||||||
# 日付と時刻を結合
|
return True
|
||||||
goal_datetime = datetime.combine(event_date, goal_time)
|
|
||||||
|
|
||||||
# JSTとして解釈
|
except Exception as e:
|
||||||
jst_tz = pytz.timezone('Asia/Tokyo')
|
print(f"❌ データベース接続エラー: {e}")
|
||||||
goal_datetime_jst = jst_tz.localize(goal_datetime)
|
return False
|
||||||
|
|
||||||
# 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):
|
def verify_location2025_compatibility(target_cursor):
|
||||||
"""Location2025テーブルとの互換性を確認"""
|
"""rog_location2025テーブルとの互換性確認"""
|
||||||
print("\n=== Location2025互換性確認 ===")
|
print("\n=== Location2025互換性確認 ===")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Location2025テーブルの存在確認
|
# テーブル存在確認
|
||||||
target_cursor.execute("""
|
target_cursor.execute("""
|
||||||
SELECT COUNT(*) FROM information_schema.tables
|
SELECT EXISTS (
|
||||||
|
SELECT FROM information_schema.tables
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND table_name = 'rog_location2025'
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
table_exists = target_cursor.fetchone()[0]
|
||||||
|
|
||||||
|
if not table_exists:
|
||||||
|
print("⚠️ rog_location2025テーブルが存在しません")
|
||||||
|
return True # テーブルが存在しない場合は互換性チェックをスキップ
|
||||||
|
|
||||||
|
# カラム構造を動的に確認
|
||||||
|
target_cursor.execute("""
|
||||||
|
SELECT column_name FROM information_schema.columns
|
||||||
WHERE table_name = 'rog_location2025'
|
WHERE table_name = 'rog_location2025'
|
||||||
|
AND table_schema = 'public'
|
||||||
""")
|
""")
|
||||||
|
columns = [row[0] for row in target_cursor.fetchall()]
|
||||||
|
print(f"検出されたカラム: {columns}")
|
||||||
|
|
||||||
table_exists = target_cursor.fetchone()[0] > 0
|
# 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 table_exists:
|
if event_column:
|
||||||
# Location2025のデータ数確認
|
# 動的にクエリを構築
|
||||||
target_cursor.execute("SELECT COUNT(*) FROM rog_location2025")
|
query = f"""
|
||||||
location2025_count = target_cursor.fetchone()[0]
|
SELECT e.{event_column}, COUNT(l.id) as location_count
|
||||||
print(f"✅ rog_location2025テーブル存在: {location2025_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()
|
||||||
|
|
||||||
# イベント別チェックポイント数確認
|
print(f"既存のLocation2025データ:")
|
||||||
target_cursor.execute("""
|
for event_id, count in location_data:
|
||||||
SELECT e.event_code, COUNT(l.id) as checkpoint_count
|
print(f" {event_column} {event_id}: {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("✅ Location2025互換性確認完了")
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
print("⚠️ rog_location2025テーブルが見つかりません")
|
print("⚠️ event_codeもevent_nameも見つかりません")
|
||||||
print("注意: 移行は可能ですが、チェックポイント管理機能は制限されます")
|
return True
|
||||||
return False
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Location2025互換性確認エラー: {e}")
|
print(f"❌ Location2025互換性確認エラー: {e}")
|
||||||
|
# トランザクションエラーの場合はロールバック
|
||||||
|
try:
|
||||||
|
target_cursor.connection.rollback()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def backup_existing_data(target_cursor):
|
def backup_existing_data(target_cursor):
|
||||||
"""既存データのバックアップ状況を確認"""
|
"""既存データのバックアップ状況を確認"""
|
||||||
print("\n=== 既存データ保護確認 ===")
|
print("\n=== 既存データ保護確認 ===")
|
||||||
|
|
||||||
|
try:
|
||||||
# 既存データ数を確認
|
# 既存データ数を確認
|
||||||
target_cursor.execute("SELECT COUNT(*) FROM rog_entry")
|
target_cursor.execute("SELECT COUNT(*) FROM rog_entry")
|
||||||
entry_count = target_cursor.fetchone()[0]
|
entry_count = target_cursor.fetchone()[0]
|
||||||
@ -177,11 +211,21 @@ def backup_existing_data(target_cursor):
|
|||||||
print("⚠️ 既存のcore application dataが見つかりません。")
|
print("⚠️ 既存のcore application dataが見つかりません。")
|
||||||
return False
|
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):
|
def migrate_gps_data(source_cursor, target_cursor):
|
||||||
"""GPS記録データのみを移行(写真記録データは除外)"""
|
"""GPS記録データのみを移行(写真記録データは除外)"""
|
||||||
print("\n=== GPS記録データの移行 ===")
|
print("\n=== GPS記録データの移行 ===")
|
||||||
|
|
||||||
# GPS記録のみを取得(不正な写真記録データを除外)
|
try:
|
||||||
|
# GPS記録のみを取得(不正な写真記録データを除外)
|
||||||
source_cursor.execute("""
|
source_cursor.execute("""
|
||||||
SELECT
|
SELECT
|
||||||
serial_number, team_name, cp_number, record_time,
|
serial_number, team_name, cp_number, record_time,
|
||||||
@ -221,7 +265,7 @@ def migrate_gps_data(source_cursor, target_cursor):
|
|||||||
elif isinstance(goal_time, datetime):
|
elif isinstance(goal_time, datetime):
|
||||||
goal_time_utc = convert_utc_to_jst(goal_time)
|
goal_time_utc = convert_utc_to_jst(goal_time)
|
||||||
|
|
||||||
# rog_gpscheckinに挿入(マイグレーション用マーカー付き)
|
# rog_gpscheckinに挿入(マイグレーション用マーカー付き)
|
||||||
target_cursor.execute("""
|
target_cursor.execute("""
|
||||||
INSERT INTO rog_gpscheckin
|
INSERT INTO rog_gpscheckin
|
||||||
(serial_number, team_name, cp_number, record_time, goal_time,
|
(serial_number, team_name, cp_number, record_time, goal_time,
|
||||||
@ -238,92 +282,87 @@ def migrate_gps_data(source_cursor, target_cursor):
|
|||||||
|
|
||||||
if migrated_count % 1000 == 0:
|
if migrated_count % 1000 == 0:
|
||||||
print(f" 移行進捗: {migrated_count}/{len(gps_records)} 件")
|
print(f" 移行進捗: {migrated_count}/{len(gps_records)} 件")
|
||||||
|
target_cursor.connection.commit()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_count += 1
|
error_count += 1
|
||||||
print(f"移行エラー (record {serial_number}): {e}")
|
print(f" レコード移行エラー(serial_number={serial_number}): {e}")
|
||||||
continue
|
if error_count > 100: # エラー上限
|
||||||
|
print("❌ エラー数が上限を超えました。移行を中止します。")
|
||||||
|
raise
|
||||||
|
|
||||||
print(f"\n移行完了: {migrated_count}件成功, {error_count}件エラー")
|
target_cursor.connection.commit()
|
||||||
|
print(f"✅ GPS記録移行完了: {migrated_count}件成功, {error_count}件エラー")
|
||||||
return migrated_count
|
return migrated_count
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ GPS記録移行エラー: {e}")
|
||||||
|
target_cursor.connection.rollback()
|
||||||
|
raise
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""メイン移行処理(既存データ保護版)"""
|
"""メイン移行処理(既存データ保護版)"""
|
||||||
print("=== 既存データ保護版移行プログラム開始 ===")
|
print("=" * 60)
|
||||||
print("注意: 既存のentry、team、memberデータは削除されません")
|
print("GPS記録データ移行スクリプト (既存データ保護版)")
|
||||||
|
print("=" * 60)
|
||||||
# データベース接続設定
|
print("移行対象: gifuroge.gps_information → rogdb.rog_gpscheckin")
|
||||||
source_config = {
|
print("既存データ保護: rog_entry, rog_team, rog_member, rog_location2025")
|
||||||
'host': 'postgres-db',
|
print("=" * 60)
|
||||||
'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:
|
try:
|
||||||
# データベース接続
|
# 1. データベース接続確認
|
||||||
print("データベースに接続中...")
|
if not check_database_connectivity():
|
||||||
source_conn = psycopg2.connect(**source_config)
|
return False
|
||||||
target_conn = psycopg2.connect(**target_config)
|
|
||||||
|
# 2. 実際の移行処理
|
||||||
|
source_conn = psycopg2.connect(**GIFUROGE_DB)
|
||||||
|
target_conn = psycopg2.connect(**ROGDB_DB)
|
||||||
|
|
||||||
source_cursor = source_conn.cursor()
|
source_cursor = source_conn.cursor()
|
||||||
target_cursor = target_conn.cursor()
|
target_cursor = target_conn.cursor()
|
||||||
|
|
||||||
# Location2025互換性確認
|
# 3. 既存データ確認
|
||||||
location2025_available = verify_location2025_compatibility(target_cursor)
|
|
||||||
|
|
||||||
# 既存データ保護確認
|
|
||||||
has_existing_data = backup_existing_data(target_cursor)
|
has_existing_data = backup_existing_data(target_cursor)
|
||||||
|
|
||||||
# 確認プロンプト
|
# 4. Location2025互換性確認
|
||||||
print(f"\nLocation2025対応: {'✅ 利用可能' if location2025_available else '⚠️ 制限あり'}")
|
is_compatible = verify_location2025_compatibility(target_cursor)
|
||||||
print(f"既存データ保護: {'✅ 検出済み' if has_existing_data else '⚠️ 未検出'}")
|
if not is_compatible:
|
||||||
|
print("❌ Location2025互換性に問題があります。")
|
||||||
|
return False
|
||||||
|
|
||||||
response = input("\n移行を開始しますか? (y/N): ")
|
# 5. 安全確認
|
||||||
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:
|
if has_existing_data:
|
||||||
print("✅ 既存のentry、team、member、location2025データは保護されました")
|
print("\n⚠️ 既存のアプリケーションデータが検出されました。")
|
||||||
else:
|
print("この移行操作は既存データを保護しながらGPS記録のみを移行します。")
|
||||||
print("⚠️ 既存のcore application dataがありませんでした")
|
confirm = input("続行しますか? (yes/no): ")
|
||||||
print(" 別途testdb/rogdb.sqlからの復元が必要です")
|
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:
|
except Exception as e:
|
||||||
print(f"移行エラー: {e}")
|
print(f"\n❌ 移行処理エラー: {e}")
|
||||||
if target_conn:
|
return False
|
||||||
target_conn.rollback()
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
if source_conn:
|
try:
|
||||||
source_conn.close()
|
source_conn.close()
|
||||||
if target_conn:
|
|
||||||
target_conn.close()
|
target_conn.close()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
success = main()
|
||||||
|
sys.exit(0 if success else 1)
|
||||||
|
|||||||
373
migration_data_protection_broken.py
Normal file
373
migration_data_protection_broken.py
Normal file
@ -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()
|
||||||
Reference in New Issue
Block a user