From 8e3f7024a2625d07724a881761df152c1fc8554a Mon Sep 17 00:00:00 2001 From: Akira Date: Mon, 25 Aug 2025 18:49:33 +0900 Subject: [PATCH] Fix migration --- MIGRATE_OLD_ROGDB_README.md | 205 ++++++++++++ MIGRATION_STATISTICS_README.md | 148 +++++++++ Makefile | 62 ++++ check_column_names.py | 175 ++++++++++ check_null_values.py | 179 ++++++++++ external_db_connection_test.py | 59 ++++ migrate_old_rogdb_quickstart.sh | 93 ++++++ migrate_old_rogdb_to_rogdb.py | 493 ++++++++++++++++++++++++++++ migrate_old_rogdb_to_rogdb_fixed.py | 414 +++++++++++++++++++++++ migration_data_protection.py | 18 +- migration_location2025_support.py | 22 +- migration_statistics.py | 406 +++++++++++++++++++++++ 12 files changed, 2254 insertions(+), 20 deletions(-) create mode 100644 MIGRATE_OLD_ROGDB_README.md create mode 100644 MIGRATION_STATISTICS_README.md create mode 100644 check_column_names.py create mode 100644 check_null_values.py create mode 100644 external_db_connection_test.py create mode 100644 migrate_old_rogdb_quickstart.sh create mode 100644 migrate_old_rogdb_to_rogdb.py create mode 100644 migrate_old_rogdb_to_rogdb_fixed.py create mode 100644 migration_statistics.py diff --git a/MIGRATE_OLD_ROGDB_README.md b/MIGRATE_OLD_ROGDB_README.md new file mode 100644 index 0000000..20af9d8 --- /dev/null +++ b/MIGRATE_OLD_ROGDB_README.md @@ -0,0 +1,205 @@ +# Old RogDB → RogDB データ移行ガイド (エラー修正版) + +## 概要 +old_rogdb データベースの `rog_*` テーブルから rogdb データベースの `rog_*` テーブルへデータを移行するスクリプトです。 + +## 修正点 (v3) +- PostgreSQL予約語(`like`など)のカラム名をクォートで囲む対応 +- **キャメルケースカラム名**(`hasGoaled`, `deadlineDateTime`など)の自動クォート対応 +- **NULL値の自動処理**:NOT NULL制約違反を防ぐデフォルト値設定 +- トランザクションエラー時の自動ロールバック機能強化 +- データベース接続のautocommit設定でトランザクション問題を回避 +- より堅牢なエラーハンドリング +- カラム名事前チェック機能の追加 +- NULL値事前チェック機能の追加 + +### NULL値デフォルト設定 +以下のカラムで自動的にデフォルト値を設定: +- `trial`, `is_trial`: `False` +- `is_active`: `True` +- `hasGoaled`, `hasParticipated`: `False` +- `public`, `class_*`: `True` +- その他のBoolean型: 一般的なデフォルト値 + +## 機能 +- 自動テーブル構造比較 +- UPSERT操作(存在する場合は更新、しない場合は挿入) +- 主キーベースの重複チェック +- 詳細な移行統計レポート +- 予約語カラムの自動クォート処理 +- エラーハンドリングとロールバック + +## 使用方法 + +### 1. Docker Compose での実行 + +```bash +# 基本実行 +docker compose exec app python migrate_old_rogdb_to_rogdb.py + +# 環境変数を使用した実行 +docker compose exec -e OLD_ROGDB_HOST=old-postgres app python migrate_old_rogdb_to_rogdb.py + +# 特定テーブルを除外 +docker compose exec -e EXCLUDE_TABLES=rog_customuser,rog_session app python migrate_old_rogdb_to_rogdb.py +``` + +### 2. Makefileタスクの使用 + +```bash +# 基本移行 +make migrate-old-rogdb + +# カラム名チェックのみ +make check-columns + +# NULL値チェックのみ +make check-null-values + +# 完全な移行前チェック(カラム名 + NULL値) +make pre-migration-check + +# 安全な移行(カラム名チェック + 移行実行) +make migrate-old-rogdb-safe + +# 統計情報のみ表示 +make migrate-rogdb-stats + +# ドライラン(テーブル一覧のみ表示) +make migrate-rogdb-dryrun +``` + +## 環境変数 + +### Old RogDB 接続設定 +```bash +OLD_ROGDB_HOST=postgres-db # デフォルト: postgres-db +OLD_ROGDB_NAME=old_rogdb # デフォルト: old_rogdb +OLD_ROGDB_USER=admin # デフォルト: admin +OLD_ROGDB_PASSWORD=admin123456 # デフォルト: admin123456 +OLD_ROGDB_PORT=5432 # デフォルト: 5432 +``` + +### RogDB 接続設定 +```bash +ROGDB_HOST=postgres-db # デフォルト: postgres-db +ROGDB_NAME=rogdb # デフォルト: rogdb +ROGDB_USER=admin # デフォルト: admin +ROGDB_PASSWORD=admin123456 # デフォルト: admin123456 +ROGDB_PORT=5432 # デフォルト: 5432 +``` + +### その他の設定 +```bash +EXCLUDE_TABLES=table1,table2 # 除外するテーブル(カンマ区切り) +``` + +## 移行対象テーブル + +スクリプトは `rog_` で始まる全てのテーブルを自動検出し、以下の処理を行います: + +### 主要テーブル(例) +- `rog_customuser` - ユーザー情報 +- `rog_newevent2` - イベント情報 +- `rog_team` - チーム情報 +- `rog_member` - メンバー情報 +- `rog_entry` - エントリー情報 +- `rog_location2025` - チェックポイント情報 +- `rog_checkpoint` - チェックポイント記録 +- その他 `rog_*` テーブル + +### 移行ロジック +1. **テーブル構造比較**: 共通カラムのみを移行対象とする +2. **主キーチェック**: 既存レコードの有無を確認 +3. **UPSERT操作**: + - 存在する場合: UPDATE(主キー以外のカラムを更新) + - 存在しない場合: INSERT(新規追加) + +## 出力例 + +``` +================================================================================ +Old RogDB → RogDB データ移行開始 +================================================================================ +データベースに接続中... +✅ データベース接続成功 +old_rogdb rog_テーブル: 15個 +rogdb rog_テーブル: 15個 +共通 rog_テーブル: 15個 +移行対象テーブル (15個): ['rog_customuser', 'rog_newevent2', ...] + +=== rog_customuser データ移行開始 === +共通カラム (12個): ['date_joined', 'email', 'first_name', ...] +主キー: ['id'] +移行対象レコード数: 50件 + 進捗: 50/50 件処理完了 +✅ rog_customuser 移行完了: + 挿入: 25件 + 更新: 25件 + エラー: 0件 + +================================================================================ +移行完了サマリー +================================================================================ +処理対象テーブル: 15個 +総挿入件数: 1250件 +総更新件数: 750件 +総エラー件数: 0件 + +--- テーブル別詳細 --- +rog_customuser: 挿入25, 更新25, エラー0 +rog_newevent2: 挿入10, 更新5, エラー0 +... +✅ 全ての移行が正常に完了しました! +``` + +## 注意事項 + +1. **バックアップ推奨**: 移行前にrogdbのバックアップを取得してください +2. **権限確認**: 両データベースへの読み書き権限が必要です +3. **外部キー制約**: 移行順序によっては外部キー制約エラーが発生する可能性があります +4. **大量データ**: 大量データの場合は時間がかかる場合があります + +## トラブルシューティング + +### よくあるエラー + +#### 1. 接続エラー +``` +❌ データベース接続エラー: connection refused +``` +**対処法**: データベースサービスが起動していることを確認 + +#### 2. 権限エラー +``` +❌ テーブル移行エラー: permission denied +``` +**対処法**: データベースユーザーの権限を確認 + +#### 3. 外部キー制約エラー +``` +❌ レコード処理エラー: foreign key constraint +``` +**対処法**: 依存関係のあるテーブルから先に移行 + +### デバッグ方法 + +```bash +# ログレベルを上げて詳細情報を表示 +docker compose exec app python -c " +import logging +logging.basicConfig(level=logging.DEBUG) +exec(open('migrate_old_rogdb_to_rogdb.py').read()) +" + +# 特定テーブルのみテスト +docker compose exec app python -c " +from migrate_old_rogdb_to_rogdb import RogTableMigrator +migrator = RogTableMigrator() +migrator.connect_databases() +migrator.migrate_table_data('rog_customuser') +" +``` + +## ライセンス +このスクリプトはMITライセンスの下で公開されています。 diff --git a/MIGRATION_STATISTICS_README.md b/MIGRATION_STATISTICS_README.md new file mode 100644 index 0000000..737283f --- /dev/null +++ b/MIGRATION_STATISTICS_README.md @@ -0,0 +1,148 @@ +# 移行結果統計情報表示スクリプト + +## 概要 + +移行処理の結果を詳細な統計情報として表示するスクリプトです。Docker Compose環境で実行可能で、移行データの品質チェックや分析に役立ちます。 + +## 実行方法 + +### 1. Docker Composeで実行 + +```bash +# 統計情報を表示 +docker compose exec app python migration_statistics.py + +# または Makeタスクを使用 +make migration-stats +``` + +### 2. 他の移行関連コマンド + +```bash +# 移行実行 +make migration-run + +# Location2025移行 +make migration-location2025 + +# データ保護移行 +make migration-data-protection + +# データベースシェル +make db-shell + +# アプリケーションログ確認 +make app-logs +``` + +## 表示される統計情報 + +### 📊 基本統計情報 +- 各テーブルのレコード数 +- 全体のデータ量概要 + +### 🎯 イベント別統計 +- 登録イベント一覧 +- イベント別参加チーム数、メンバー数、エントリー数 + +### 📍 GPSチェックイン統計 +- 総チェックイン数、参加チーム数 +- 時間帯別チェックイン分布 +- CP利用ランキング(上位10位) + +### 👥 チーム統計 +- 総チーム数、クラス数 +- クラス別チーム分布 +- 平均メンバー数 + +### 🔍 データ品質チェック +- 重複データチェック +- 異常時刻データチェック +- データ整合性チェック + +### 📄 JSON出力 +- 統計情報をJSONファイルで出力 +- 外部システムでの利用や保存に便利 + +## 出力例 + +``` +================================================================================ +📊 移行データ基本統計情報 +================================================================================ + +📋 テーブル別レコード数: +テーブル名 日本語名 レコード数 +----------------------------------------------------------------- +rog_newevent2 イベント 12件 +rog_team チーム 450件 +rog_member メンバー 1,200件 +rog_entry エントリー 450件 +rog_gpscheckin GPSチェックイン 8,500件 +rog_checkpoint チェックポイント 800件 +rog_location2025 ロケーション2025 50件 +rog_customuser ユーザー 25件 +----------------------------------------------------------------- +合計 11,487件 + +================================================================================ +🎯 イベント別統計情報 +================================================================================ + +📅 登録イベント数: 12件 + +イベント詳細: +ID イベント名 開催日 登録日時 +------------------------------------------------------------ + 1 美濃加茂 2024-05-19 2024-08-25 10:30 + 2 岐阜市 2024-04-28 2024-08-25 10:30 + 3 大垣2 2024-04-20 2024-08-25 10:30 +... +``` + +## トラブルシューティング + +### データベース接続エラー +```bash +# コンテナの状態確認 +docker compose ps + +# PostgreSQLログ確認 +make db-logs + +# アプリケーションログ確認 +make app-logs +``` + +### 環境変数の確認 +```bash +# 環境変数が正しく設定されているか確認 +docker compose exec app env | grep POSTGRES +``` + +### 手動でのデータベース接続テスト +```bash +# PostgreSQLコンテナに直接接続 +docker compose exec postgres-db psql -U admin -d rogdb + +# テーブル確認 +\dt + +# 基本的なクエリテスト +SELECT COUNT(*) FROM rog_gpscheckin; +``` + +## 関連ファイル + +- `migration_statistics.py` - 統計表示メインスクリプト +- `migration_final_simple.py` - GPS記録移行スクリプト +- `migration_location2025_support.py` - Location2025移行スクリプト +- `migration_data_protection.py` - データ保護移行スクリプト +- `Makefile` - 実行用タスク定義 + +## 注意事項 + +- Docker Composeが正常に起動していることを確認してください +- PostgreSQLコンテナが稼働していることを確認してください +- 統計情報は実行時点のデータベース状態を反映します +- JSON出力ファイルは `/app/` ディレクトリに保存されます diff --git a/Makefile b/Makefile index b68225c..cd18721 100644 --- a/Makefile +++ b/Makefile @@ -31,3 +31,65 @@ volume: shell: docker-compose exec api python3 manage.py shell +# 移行関連タスク +migration-stats: + docker compose exec app python migration_statistics.py + +migration-run: + docker compose exec app python migration_final_simple.py + +migration-location2025: + docker compose exec app python migration_location2025_support.py + +migration-data-protection: + docker compose exec app python migration_data_protection.py + +# Old RogDB → RogDB 移行 +migrate-old-rogdb: + docker compose exec app python migrate_old_rogdb_to_rogdb.py + +# カラム名チェック +check-columns: + docker compose exec app python check_column_names.py + +# NULL値チェック +check-null-values: + docker compose exec app python check_null_values.py + +# 完全な移行前チェック +pre-migration-check: + @echo "=== カラム名チェック ===" + docker compose exec app python check_column_names.py + @echo "=== NULL値チェック ===" + docker compose exec app python check_null_values.py + +# 移行前準備(カラム名チェック + 移行実行) +migrate-old-rogdb-safe: + @echo "=== カラム名チェック実行 ===" + docker compose exec app python check_column_names.py + @echo "=== 移行実行 ===" + docker compose exec app python migrate_old_rogdb_to_rogdb.py + +migrate-old-rogdb-stats: + docker compose exec app python -c "from migrate_old_rogdb_to_rogdb import RogTableMigrator; m = RogTableMigrator(); m.connect_databases(); m.get_rog_tables()" + +migrate-old-rogdb-dryrun: + docker compose exec -e EXCLUDE_TABLES=all app python migrate_old_rogdb_to_rogdb.py + +migrate-old-rogdb-exclude-users: + docker compose exec -e EXCLUDE_TABLES=rog_customuser,rog_session app python migrate_old_rogdb_to_rogdb.py + +# データベース関連 +db-shell: + docker compose exec postgres-db psql -U $(POSTGRES_USER) -d $(POSTGRES_DBNAME) + +db-backup: + docker compose exec postgres-db pg_dump -U $(POSTGRES_USER) $(POSTGRES_DBNAME) > backup_$(shell date +%Y%m%d_%H%M%S).sql + +# ログ確認 +app-logs: + docker compose logs app --tail=100 -f + +db-logs: + docker compose logs postgres-db --tail=50 -f + diff --git a/check_column_names.py b/check_column_names.py new file mode 100644 index 0000000..776217d --- /dev/null +++ b/check_column_names.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 +""" +カラム名検証スクリプト +PostgreSQLで問題となるカラム名を事前にチェック +""" + +import os +import psycopg2 +import logging + +# ログ設定 +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + +# データベース設定 +ROGDB_CONFIG = { + 'host': os.getenv('ROGDB_HOST', 'postgres-db'), + 'database': os.getenv('ROGDB_NAME', 'rogdb'), + 'user': os.getenv('ROGDB_USER', 'admin'), + 'password': os.getenv('ROGDB_PASSWORD', 'admin123456'), + 'port': int(os.getenv('ROGDB_PORT', 5432)) +} + +# PostgreSQL予約語 +RESERVED_KEYWORDS = { + 'like', 'order', 'group', 'user', 'table', 'where', 'select', 'insert', + 'update', 'delete', 'create', 'drop', 'alter', 'index', 'constraint', + 'default', 'check', 'unique', 'primary', 'foreign', 'key', 'references' +} + +def check_column_names(): + """全rog_テーブルのカラム名をチェック""" + try: + conn = psycopg2.connect(**ROGDB_CONFIG) + cursor = conn.cursor() + + # rog_テーブル一覧取得 + cursor.execute(""" + SELECT table_name + FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name LIKE 'rog_%' + ORDER BY table_name + """) + tables = [row[0] for row in cursor.fetchall()] + + logger.info(f"チェック対象テーブル: {len(tables)}個") + + problematic_columns = {} + + for table_name in tables: + # テーブルのカラム一覧取得 + cursor.execute(""" + SELECT column_name + FROM information_schema.columns + WHERE table_name = %s + AND table_schema = 'public' + ORDER BY ordinal_position + """, (table_name,)) + + columns = [row[0] for row in cursor.fetchall()] + problem_cols = [] + + for col in columns: + # 予約語チェック + if col.lower() in RESERVED_KEYWORDS: + problem_cols.append((col, '予約語')) + + # キャメルケース/大文字チェック + elif any(c.isupper() for c in col) or col != col.lower(): + problem_cols.append((col, 'キャメルケース/大文字')) + + if problem_cols: + problematic_columns[table_name] = problem_cols + + # 結果出力 + if problematic_columns: + logger.warning("⚠️ 問題のあるカラム名が見つかりました:") + for table, cols in problematic_columns.items(): + logger.warning(f" {table}:") + for col, reason in cols: + logger.warning(f" - {col} ({reason})") + else: + logger.info("✅ 全てのカラム名は問題ありません") + + # クォートが必要なカラムのリスト生成 + need_quotes = set() + for table, cols in problematic_columns.items(): + for col, reason in cols: + need_quotes.add(col) + + if need_quotes: + logger.info("📋 クォートが必要なカラム一覧:") + for col in sorted(need_quotes): + logger.info(f" '{col}' -> '\"{col}\"'") + + cursor.close() + conn.close() + + return problematic_columns + + except Exception as e: + logger.error(f"❌ カラム名チェックエラー: {e}") + return {} + +def test_quoted_query(): + """クォート付きクエリのテスト""" + try: + conn = psycopg2.connect(**ROGDB_CONFIG) + cursor = conn.cursor() + + # 問題のあるテーブルでテストクエリ実行 + test_tables = ['rog_entry', 'rog_newevent2'] + + for table_name in test_tables: + logger.info(f"=== {table_name} クエリテスト ===") + + # カラム一覧取得 + cursor.execute(""" + SELECT column_name + FROM information_schema.columns + WHERE table_name = %s + AND table_schema = 'public' + ORDER BY ordinal_position + """, (table_name,)) + + columns = [row[0] for row in cursor.fetchall()] + + # クォート付きカラム名生成 + def quote_column_if_needed(column_name): + if column_name.lower() in RESERVED_KEYWORDS: + return f'"{column_name}"' + if any(c.isupper() for c in column_name) or column_name != column_name.lower(): + return f'"{column_name}"' + return column_name + + quoted_columns = [quote_column_if_needed(col) for col in columns] + columns_str = ', '.join(quoted_columns) + + # テストクエリ実行 + try: + test_query = f"SELECT {columns_str} FROM {table_name} LIMIT 1" + logger.info(f"テストクエリ: {test_query[:100]}...") + cursor.execute(test_query) + result = cursor.fetchone() + logger.info(f"✅ {table_name}: クエリ成功") + + except Exception as e: + logger.error(f"❌ {table_name}: クエリエラー: {e}") + + cursor.close() + conn.close() + + except Exception as e: + logger.error(f"❌ クエリテストエラー: {e}") + +def main(): + logger.info("=" * 60) + logger.info("PostgreSQL カラム名検証スクリプト") + logger.info("=" * 60) + + # カラム名チェック + problematic_columns = check_column_names() + + print() + + # クエリテスト + test_quoted_query() + + logger.info("=" * 60) + logger.info("検証完了") + logger.info("=" * 60) + +if __name__ == "__main__": + main() diff --git a/check_null_values.py b/check_null_values.py new file mode 100644 index 0000000..f5bef57 --- /dev/null +++ b/check_null_values.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 +""" +NULL値チェック・デフォルト値テストスクリプト +""" + +import os +import psycopg2 +import logging + +# ログ設定 +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + +# データベース設定 +OLD_ROGDB_CONFIG = { + 'host': os.getenv('OLD_ROGDB_HOST', 'postgres-db'), + 'database': os.getenv('OLD_ROGDB_NAME', 'old_rogdb'), + 'user': os.getenv('OLD_ROGDB_USER', 'admin'), + 'password': os.getenv('OLD_ROGDB_PASSWORD', 'admin123456'), + 'port': int(os.getenv('OLD_ROGDB_PORT', 5432)) +} + +ROGDB_CONFIG = { + 'host': os.getenv('ROGDB_HOST', 'postgres-db'), + 'database': os.getenv('ROGDB_NAME', 'rogdb'), + 'user': os.getenv('ROGDB_USER', 'admin'), + 'password': os.getenv('ROGDB_PASSWORD', 'admin123456'), + 'port': int(os.getenv('ROGDB_PORT', 5432)) +} + +def check_null_values(): + """NULL値の問題を事前チェック""" + try: + old_conn = psycopg2.connect(**OLD_ROGDB_CONFIG) + new_conn = psycopg2.connect(**ROGDB_CONFIG) + + old_conn.autocommit = True + new_conn.autocommit = True + + old_cursor = old_conn.cursor() + new_cursor = new_conn.cursor() + + # 共通テーブル取得 + old_cursor.execute(""" + SELECT table_name FROM information_schema.tables + WHERE table_schema = 'public' AND table_name LIKE 'rog_%' + """) + old_tables = [row[0] for row in old_cursor.fetchall()] + + new_cursor.execute(""" + SELECT table_name FROM information_schema.tables + WHERE table_schema = 'public' AND table_name LIKE 'rog_%' + """) + new_tables = [row[0] for row in new_cursor.fetchall()] + + common_tables = list(set(old_tables) & set(new_tables)) + + logger.info(f"チェック対象テーブル: {len(common_tables)}個") + + null_issues = {} + + for table_name in common_tables: + logger.info(f"=== {table_name} NULL値チェック ===") + + # 新しいDBのNOT NULL制約確認 + new_cursor.execute(""" + SELECT column_name, is_nullable, column_default + FROM information_schema.columns + WHERE table_name = %s AND table_schema = 'public' + AND is_nullable = 'NO' + ORDER BY ordinal_position + """, (table_name,)) + + not_null_columns = new_cursor.fetchall() + + if not not_null_columns: + logger.info(f" NOT NULL制約なし") + continue + + logger.info(f" NOT NULL制約カラム: {[col[0] for col in not_null_columns]}") + + # 古いDBのNULL値チェック + for col_name, is_nullable, default_val in not_null_columns: + try: + # PostgreSQL予約語とcamelCaseカラムのクォート処理 + reserved_words = ['group', 'like', 'order', 'user', 'table', 'index', 'where', 'from', 'select'] + quoted_col = f'"{col_name}"' if (col_name.lower() in reserved_words or any(c.isupper() for c in col_name)) else col_name + + # カラム存在チェック + old_cursor.execute(""" + SELECT COUNT(*) FROM information_schema.columns + WHERE table_name = %s AND column_name = %s AND table_schema = 'public' + """, (table_name, col_name)) + + if old_cursor.fetchone()[0] == 0: + logger.warning(f" ⚠️ {col_name}: 古いDBに存在しないカラム") + continue + + old_cursor.execute(f""" + SELECT COUNT(*) FROM {table_name} + WHERE {quoted_col} IS NULL + """) + + null_count = old_cursor.fetchone()[0] + + if null_count > 0: + logger.warning(f" ⚠️ {col_name}: {null_count}件のNULL値あり (デフォルト: {default_val})") + + if table_name not in null_issues: + null_issues[table_name] = [] + null_issues[table_name].append((col_name, null_count, default_val)) + else: + logger.info(f" ✅ {col_name}: NULL値なし") + + except Exception as e: + logger.error(f" ❌ {col_name}: チェックエラー: {e}") + + # サマリー + if null_issues: + logger.warning("=" * 60) + logger.warning("NULL値問題のあるテーブル:") + for table, issues in null_issues.items(): + logger.warning(f" {table}:") + for col, count, default in issues: + logger.warning(f" - {col}: {count}件 (デフォルト: {default})") + else: + logger.info("✅ NULL値の問題はありません") + + old_cursor.close() + new_cursor.close() + old_conn.close() + new_conn.close() + + return null_issues + + except Exception as e: + logger.error(f"❌ NULL値チェックエラー: {e}") + return {} + +def suggest_default_values(null_issues): + """デフォルト値の提案""" + if not null_issues: + return + + logger.info("=" * 60) + logger.info("推奨デフォルト値設定:") + + for table_name, issues in null_issues.items(): + logger.info(f" '{table_name}': {{") + for col_name, count, default in issues: + # データ型に基づくデフォルト値推測 + if 'trial' in col_name.lower() or 'is_' in col_name.lower(): + suggested = 'False' + elif 'public' in col_name.lower(): + suggested = 'True' + elif 'name' in col_name.lower() or 'description' in col_name.lower(): + suggested = "''" + elif 'order' in col_name.lower() or 'sort' in col_name.lower(): + suggested = '0' + else: + suggested = 'None # 要確認' + + logger.info(f" '{col_name}': {suggested}, # {count}件のNULL") + logger.info(" },") + +def main(): + logger.info("=" * 60) + logger.info("NULL値チェック・デフォルト値提案スクリプト") + logger.info("=" * 60) + + null_issues = check_null_values() + suggest_default_values(null_issues) + + logger.info("=" * 60) + logger.info("チェック完了") + logger.info("=" * 60) + +if __name__ == "__main__": + main() diff --git a/external_db_connection_test.py b/external_db_connection_test.py new file mode 100644 index 0000000..31bae5d --- /dev/null +++ b/external_db_connection_test.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +""" +外部スクリプトからDBコンテナに接続するサンプル +""" +import os +import psycopg2 +from psycopg2.extras import DictCursor + +# 環境変数から接続情報を取得 +DB_CONFIG = { + 'host': os.getenv('PG_HOST', 'localhost'), + 'port': os.getenv('PG_PORT', '5432'), + 'database': os.getenv('POSTGRES_DBNAME', 'rogdb'), + 'user': os.getenv('POSTGRES_USER', 'admin'), + 'password': os.getenv('POSTGRES_PASS', 'admin123456') +} + +def connect_to_db(): + """データベースに接続""" + try: + conn = psycopg2.connect(**DB_CONFIG) + print(f"✅ データベースに接続成功: {DB_CONFIG['host']}:{DB_CONFIG['port']}") + return conn + except psycopg2.Error as e: + print(f"❌ データベース接続エラー: {e}") + return None + +def test_connection(): + """接続テスト""" + conn = connect_to_db() + if conn: + try: + with conn.cursor(cursor_factory=DictCursor) as cur: + cur.execute("SELECT version();") + version = cur.fetchone() + print(f"PostgreSQL バージョン: {version[0]}") + + # テーブル一覧を取得 + cur.execute(""" + SELECT tablename FROM pg_tables + WHERE schemaname = 'public' + ORDER BY tablename; + """) + tables = cur.fetchall() + print(f"テーブル数: {len(tables)}") + for table in tables[:5]: # 最初の5個を表示 + print(f" - {table[0]}") + + except psycopg2.Error as e: + print(f"❌ クエリ実行エラー: {e}") + finally: + conn.close() + +if __name__ == "__main__": + print("=== データベース接続テスト ===") + print(f"接続先: {DB_CONFIG['host']}:{DB_CONFIG['port']}") + print(f"データベース: {DB_CONFIG['database']}") + print(f"ユーザー: {DB_CONFIG['user']}") + test_connection() diff --git a/migrate_old_rogdb_quickstart.sh b/migrate_old_rogdb_quickstart.sh new file mode 100644 index 0000000..eaca95c --- /dev/null +++ b/migrate_old_rogdb_quickstart.sh @@ -0,0 +1,93 @@ +#!/bin/bash +# Old RogDB → RogDB 移行クイックスタート + +echo "============================================================" +echo "Old RogDB → RogDB データ移行クイックスタート" +echo "============================================================" + +# 実行前チェック +echo "🔍 実行前チェック..." + +# Docker Composeサービス確認 +if ! docker compose ps | grep -q "Up"; then + echo "❌ Docker Composeサービスが起動していません" + echo "次のコマンドでサービスを起動してください:" + echo "docker compose up -d" + exit 1 +fi + +echo "✅ Docker Composeサービス確認完了" + +# データベース接続確認 +echo "🔍 データベース接続確認..." +if ! docker compose exec app python -c " +import psycopg2 +try: + conn = psycopg2.connect(host='postgres-db', database='rogdb', user='admin', password='admin123456') + print('✅ rogdb接続成功') + conn.close() +except Exception as e: + print(f'❌ rogdb接続エラー: {e}') + exit(1) +"; then + echo "❌ データベース接続に失敗しました" + exit 1 +fi + +# メニュー表示 +echo "" +echo "📋 移行オプションを選択してください:" +echo "1) 完全移行(全rog_*テーブル)" +echo "2) 安全移行(ユーザー関連テーブル除外)" +echo "3) 統計情報のみ表示" +echo "4) テーブル一覧のみ表示" +echo "5) カスタム移行(除外テーブル指定)" +echo "0) キャンセル" +echo "" + +read -p "選択 (0-5): " choice + +case $choice in + 1) + echo "🚀 完全移行を開始します..." + docker compose exec app python migrate_old_rogdb_to_rogdb.py + ;; + 2) + echo "🛡️ 安全移行を開始します(ユーザー関連テーブル除外)..." + docker compose exec -e EXCLUDE_TABLES=rog_customuser,rog_session app python migrate_old_rogdb_to_rogdb.py + ;; + 3) + echo "📊 統計情報を表示します..." + make migrate-old-rogdb-stats + ;; + 4) + echo "📋 テーブル一覧を表示します..." + make migrate-old-rogdb-dryrun + ;; + 5) + echo "除外するテーブル名をカンマ区切りで入力してください(例: rog_customuser,rog_session):" + read -p "除外テーブル: " exclude_tables + echo "🔧 カスタム移行を開始します..." + docker compose exec -e EXCLUDE_TABLES="$exclude_tables" app python migrate_old_rogdb_to_rogdb.py + ;; + 0) + echo "移行をキャンセルしました" + exit 0 + ;; + *) + echo "❌ 無効な選択です" + exit 1 + ;; +esac + +echo "" +echo "============================================================" +echo "移行処理が完了しました" +echo "============================================================" +echo "" +echo "📋 移行後の確認方法:" +echo " - ログ確認: make app-logs" +echo " - DB接続: make db-shell" +echo " - 統計再表示: make migrate-old-rogdb-stats" +echo "" +echo "📖 詳細情報: MIGRATE_OLD_ROGDB_README.md" diff --git a/migrate_old_rogdb_to_rogdb.py b/migrate_old_rogdb_to_rogdb.py new file mode 100644 index 0000000..944d034 --- /dev/null +++ b/migrate_old_rogdb_to_rogdb.py @@ -0,0 +1,493 @@ +#!/usr/bin/env python3 +""" +Old RogDB データ移行スクリプト (エラー修正版) +old_rogdb の rog_* テーブルから rogdb の rog_* テーブルへデータを更新・挿入 +""" + +import os +import sys +import psycopg2 +from datetime import datetime, timezone +from typing import Optional, Dict, List, Tuple +import logging + +# ログ設定 +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + +# データベース設定 +OLD_ROGDB_CONFIG = { + 'host': os.getenv('OLD_ROGDB_HOST', 'postgres-db'), + 'database': os.getenv('OLD_ROGDB_NAME', 'old_rogdb'), + 'user': os.getenv('OLD_ROGDB_USER', 'admin'), + 'password': os.getenv('OLD_ROGDB_PASSWORD', 'admin123456'), + 'port': int(os.getenv('OLD_ROGDB_PORT', 5432)) +} + +ROGDB_CONFIG = { + 'host': os.getenv('ROGDB_HOST', 'postgres-db'), + 'database': os.getenv('ROGDB_NAME', 'rogdb'), + 'user': os.getenv('ROGDB_USER', 'admin'), + 'password': os.getenv('ROGDB_PASSWORD', 'admin123456'), + 'port': int(os.getenv('ROGDB_PORT', 5432)) +} + +# PostgreSQL予約語をクォートで囲む必要があるカラム +RESERVED_KEYWORDS = { + 'like', 'order', 'group', 'user', 'table', 'where', 'select', 'insert', + 'update', 'delete', 'create', 'drop', 'alter', 'index', 'constraint' +} + +class RogTableMigrator: + """Rogテーブル移行クラス (エラー修正版)""" + + def __init__(self): + self.old_conn = None + self.new_conn = None + self.old_cursor = None + self.new_cursor = None + self.migration_stats = {} + + def quote_column_if_needed(self, column_name): + """予約語やキャメルケースの場合はダブルクォートで囲む""" + # 予約語チェック + if column_name.lower() in RESERVED_KEYWORDS: + return f'"{column_name}"' + + # キャメルケースや大文字が含まれる場合もクォート + if any(c.isupper() for c in column_name) or column_name != column_name.lower(): + return f'"{column_name}"' + + return column_name + + def handle_null_values(self, table_name, column_name, value): + """NULL値の処理とデフォルト値設定""" + if value is not None: + return value + + # テーブル・カラム別のデフォルト値設定 + null_defaults = { + 'rog_team': { + 'trial': False, # Boolean型のデフォルト + 'is_active': True, + 'is_trial': False, + }, + 'rog_entry': { + 'trial': False, + 'is_active': True, + 'is_trial': False, + 'hasGoaled': False, + 'hasParticipated': False, + }, + 'rog_member': { + 'is_active': True, + 'is_captain': False, + }, + 'rog_newevent2': { + 'public': True, + 'class_general': True, + 'class_family': True, + 'class_solo_male': True, + 'class_solo_female': True, + 'hour_3': True, + 'hour_5': True, + 'self_rogaining': False, + } + } + + # テーブル固有のデフォルト値を取得 + if table_name in null_defaults and column_name in null_defaults[table_name]: + default_value = null_defaults[table_name][column_name] + logger.debug(f"NULL値をデフォルト値に変換: {table_name}.{column_name} = {default_value}") + return default_value + + # 一般的なデフォルト値 + common_defaults = { + # Boolean型 + 'is_active': True, + 'is_trial': False, + 'public': True, + 'trial': False, + # 文字列型 + 'description': '', + 'note': '', + 'comment': '', + # 数値型 + 'sort_order': 0, + 'order': 0, + # PostgreSQL予約語 + 'group': '', + 'like': False, + } + + if column_name in common_defaults: + default_value = common_defaults[column_name] + logger.debug(f"NULL値を共通デフォルト値に変換: {column_name} = {default_value}") + return default_value + + # デフォルト値が見つからない場合はNULLを返す + logger.warning(f"デフォルト値が設定されていません: {table_name}.{column_name}") + return None + + def connect_databases(self): + """データベース接続""" + try: + logger.info("データベースに接続中...") + self.old_conn = psycopg2.connect(**OLD_ROGDB_CONFIG) + self.new_conn = psycopg2.connect(**ROGDB_CONFIG) + + # autocommitを有効にしてトランザクション問題を回避 + self.old_conn.autocommit = True + self.new_conn.autocommit = False # 新しい方は手動コミット + + self.old_cursor = self.old_conn.cursor() + self.new_cursor = self.new_conn.cursor() + + logger.info("✅ データベース接続成功") + return True + + except Exception as e: + logger.error(f"❌ データベース接続エラー: {e}") + return False + + def close_connections(self): + """データベース接続クローズ""" + try: + if self.old_cursor: + self.old_cursor.close() + if self.new_cursor: + self.new_cursor.close() + if self.old_conn: + self.old_conn.close() + if self.new_conn: + self.new_conn.close() + logger.info("データベース接続をクローズしました") + except Exception as e: + logger.warning(f"接続クローズ時の警告: {e}") + + def get_rog_tables(self): + """rog_で始まるテーブル一覧を取得""" + try: + # old_rogdbのrog_テーブル一覧 + self.old_cursor.execute(""" + SELECT table_name + FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name LIKE 'rog_%' + ORDER BY table_name + """) + old_tables = [row[0] for row in self.old_cursor.fetchall()] + + # rogdbのrog_テーブル一覧 + self.new_cursor.execute(""" + SELECT table_name + FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name LIKE 'rog_%' + ORDER BY table_name + """) + new_tables = [row[0] for row in self.new_cursor.fetchall()] + + # 共通テーブル + common_tables = list(set(old_tables) & set(new_tables)) + + logger.info(f"old_rogdb rog_テーブル: {len(old_tables)}個") + logger.info(f"rogdb rog_テーブル: {len(new_tables)}個") + logger.info(f"共通 rog_テーブル: {len(common_tables)}個") + + return old_tables, new_tables, common_tables + + except Exception as e: + logger.error(f"テーブル一覧取得エラー: {e}") + return [], [], [] + + def get_table_structure(self, table_name, cursor): + """テーブル構造を取得 (エラーハンドリング強化)""" + try: + cursor.execute(""" + SELECT column_name, data_type, is_nullable, column_default + FROM information_schema.columns + WHERE table_name = %s + AND table_schema = 'public' + ORDER BY ordinal_position + """, (table_name,)) + + columns = cursor.fetchall() + return { + 'columns': [col[0] for col in columns], + 'details': columns + } + except Exception as e: + logger.error(f"テーブル構造取得エラー ({table_name}): {e}") + # トランザクションエラーを回避 + try: + cursor.connection.rollback() + except: + pass + return {'columns': [], 'details': []} + + def get_primary_key(self, table_name, cursor): + """主キーカラムを取得""" + try: + cursor.execute(""" + SELECT a.attname + FROM pg_index i + JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) + WHERE i.indrelid = %s::regclass AND i.indisprimary + """, (table_name,)) + + pk_columns = [row[0] for row in cursor.fetchall()] + return pk_columns if pk_columns else ['id'] + + except Exception as e: + logger.warning(f"主キー取得エラー ({table_name}): {e}") + return ['id'] # デフォルトでidを使用 + + def migrate_table_data(self, table_name): + """個別テーブルのデータ移行 (エラー修正版)""" + logger.info(f"\n=== {table_name} データ移行開始 ===") + + try: + # テーブル構造比較 + old_structure = self.get_table_structure(table_name, self.old_cursor) + new_structure = self.get_table_structure(table_name, self.new_cursor) + + old_columns = set(old_structure['columns']) + new_columns = set(new_structure['columns']) + common_columns = old_columns & new_columns + + if not common_columns: + logger.warning(f"⚠️ {table_name}: 共通カラムがありません") + return {'inserted': 0, 'updated': 0, 'errors': 0} + + logger.info(f"共通カラム ({len(common_columns)}個): {sorted(common_columns)}") + + # 主キー取得 + pk_columns = self.get_primary_key(table_name, self.new_cursor) + logger.info(f"主キー: {pk_columns}") + + # カラム名を予約語対応でクォート + quoted_columns = [self.quote_column_if_needed(col) for col in common_columns] + columns_str = ', '.join(quoted_columns) + + # old_rogdbからデータ取得 + try: + self.old_cursor.execute(f"SELECT {columns_str} FROM {table_name}") + old_records = self.old_cursor.fetchall() + except Exception as e: + logger.error(f"❌ {table_name} データ取得エラー: {e}") + return {'inserted': 0, 'updated': 0, 'errors': 1} + + if not old_records: + logger.info(f"✅ {table_name}: 移行対象データなし") + return {'inserted': 0, 'updated': 0, 'errors': 0} + + logger.info(f"移行対象レコード数: {len(old_records)}件") + + # 統計情報 + inserted_count = 0 + updated_count = 0 + error_count = 0 + + # レコード移行処理 + for i, record in enumerate(old_records): + try: + # レコードデータを辞書形式に変換(NULL値処理込み) + record_dict = {} + for j, col in enumerate(common_columns): + original_value = record[j] + processed_value = self.handle_null_values(table_name, col, original_value) + record_dict[col] = processed_value + + # 主キー値を取得 + pk_values = [] + pk_conditions = [] + for pk_col in pk_columns: + if pk_col in record_dict: + pk_values.append(record_dict[pk_col]) + quoted_pk_col = self.quote_column_if_needed(pk_col) + pk_conditions.append(f"{quoted_pk_col} = %s") + + if not pk_values: + error_count += 1 + continue + + # 既存レコード確認 + check_query = f"SELECT COUNT(*) FROM {table_name} WHERE {' AND '.join(pk_conditions)}" + self.new_cursor.execute(check_query, pk_values) + exists = self.new_cursor.fetchone()[0] > 0 + + if exists: + # UPDATE処理 + set_clauses = [] + update_values = [] + + for col in common_columns: + if col not in pk_columns: # 主キー以外を更新 + quoted_col = self.quote_column_if_needed(col) + set_clauses.append(f"{quoted_col} = %s") + update_values.append(record_dict[col]) + + if set_clauses: + update_query = f""" + UPDATE {table_name} + SET {', '.join(set_clauses)} + WHERE {' AND '.join(pk_conditions)} + """ + self.new_cursor.execute(update_query, update_values + pk_values) + updated_count += 1 + + else: + # INSERT処理 + insert_columns = list(common_columns) + insert_values = [record_dict[col] for col in insert_columns] + quoted_insert_columns = [self.quote_column_if_needed(col) for col in insert_columns] + placeholders = ', '.join(['%s'] * len(insert_columns)) + + insert_query = f""" + INSERT INTO {table_name} ({', '.join(quoted_insert_columns)}) + VALUES ({placeholders}) + """ + self.new_cursor.execute(insert_query, insert_values) + inserted_count += 1 + + # 定期的なコミット + if (i + 1) % 100 == 0: + self.new_conn.commit() + logger.info(f" 進捗: {i + 1}/{len(old_records)} 件処理完了") + + except Exception as e: + error_count += 1 + logger.error(f" レコード処理エラー (行{i+1}): {e}") + # トランザクションロールバック + try: + self.new_conn.rollback() + except: + pass + + if error_count > 10: # エラー上限 + logger.error(f"❌ {table_name}: エラー数が上限を超えました") + break + + # 最終コミット + try: + self.new_conn.commit() + except Exception as e: + logger.error(f"コミットエラー: {e}") + + # 結果サマリー + stats = { + 'inserted': inserted_count, + 'updated': updated_count, + 'errors': error_count, + 'total': len(old_records) + } + + logger.info(f"✅ {table_name} 移行完了:") + logger.info(f" 挿入: {inserted_count}件") + logger.info(f" 更新: {updated_count}件") + logger.info(f" エラー: {error_count}件") + + self.migration_stats[table_name] = stats + return stats + + except Exception as e: + logger.error(f"❌ {table_name} 移行エラー: {e}") + try: + self.new_conn.rollback() + except: + pass + return {'inserted': 0, 'updated': 0, 'errors': 1} + + def run_migration(self, exclude_tables=None): + """全体移行実行""" + if exclude_tables is None: + exclude_tables = [] + + logger.info("=" * 80) + logger.info("Old RogDB → RogDB データ移行開始") + logger.info("=" * 80) + + try: + # データベース接続 + if not self.connect_databases(): + return False + + # テーブル一覧取得 + old_tables, new_tables, common_tables = self.get_rog_tables() + + if not common_tables: + logger.error("❌ 移行対象の共通テーブルがありません") + return False + + # 除外テーブルを除去 + migration_tables = [t for t in common_tables if t not in exclude_tables] + + if exclude_tables: + logger.info(f"除外テーブル: {exclude_tables}") + + logger.info(f"移行対象テーブル ({len(migration_tables)}個): {migration_tables}") + + # テーブル別移行実行 + total_inserted = 0 + total_updated = 0 + total_errors = 0 + + for table_name in migration_tables: + stats = self.migrate_table_data(table_name) + total_inserted += stats['inserted'] + total_updated += stats['updated'] + total_errors += stats['errors'] + + # 最終結果 + logger.info("\n" + "=" * 80) + logger.info("移行完了サマリー") + logger.info("=" * 80) + logger.info(f"処理対象テーブル: {len(migration_tables)}個") + logger.info(f"総挿入件数: {total_inserted}件") + logger.info(f"総更新件数: {total_updated}件") + logger.info(f"総エラー件数: {total_errors}件") + + # テーブル別詳細 + logger.info("\n--- テーブル別詳細 ---") + for table_name, stats in self.migration_stats.items(): + logger.info(f"{table_name}: 挿入{stats['inserted']}, 更新{stats['updated']}, エラー{stats['errors']}") + + if total_errors == 0: + logger.info("✅ 全ての移行が正常に完了しました!") + return True + else: + logger.warning(f"⚠️ {total_errors}件のエラーがありました") + return False + + except Exception as e: + logger.error(f"❌ 移行処理エラー: {e}") + return False + + finally: + self.close_connections() + +def main(): + """メイン処理""" + logger.info("Old RogDB → RogDB データ移行スクリプト") + + # 除外テーブルの設定(必要に応じて) + exclude_tables = [ + # 'rog_customuser', # ユーザーデータは慎重に扱う + # 'rog_session', # セッションデータは移行不要 + # 'django_migrations', # Django管理テーブル + ] + + # 環境変数による除外テーブル指定 + env_exclude = os.getenv('EXCLUDE_TABLES', '') + if env_exclude: + exclude_tables.extend(env_exclude.split(',')) + + # 移行実行 + migrator = RogTableMigrator() + success = migrator.run_migration(exclude_tables=exclude_tables) + + sys.exit(0 if success else 1) + +if __name__ == "__main__": + main() diff --git a/migrate_old_rogdb_to_rogdb_fixed.py b/migrate_old_rogdb_to_rogdb_fixed.py new file mode 100644 index 0000000..409be0c --- /dev/null +++ b/migrate_old_rogdb_to_rogdb_fixed.py @@ -0,0 +1,414 @@ +#!/usr/bin/env python3 +""" +Old RogDB データ移行スクリプト (エラー修正版) +old_rogdb の rog_* テーブルから rogdb の rog_* テーブルへデータを更新・挿入 +""" + +import os +import sys +import psycopg2 +from datetime import datetime, timezone +from typing import Optional, Dict, List, Tuple +import logging + +# ログ設定 +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + +# データベース設定 +OLD_ROGDB_CONFIG = { + 'host': os.getenv('OLD_ROGDB_HOST', 'postgres-db'), + 'database': os.getenv('OLD_ROGDB_NAME', 'old_rogdb'), + 'user': os.getenv('OLD_ROGDB_USER', 'admin'), + 'password': os.getenv('OLD_ROGDB_PASSWORD', 'admin123456'), + 'port': int(os.getenv('OLD_ROGDB_PORT', 5432)) +} + +ROGDB_CONFIG = { + 'host': os.getenv('ROGDB_HOST', 'postgres-db'), + 'database': os.getenv('ROGDB_NAME', 'rogdb'), + 'user': os.getenv('ROGDB_USER', 'admin'), + 'password': os.getenv('ROGDB_PASSWORD', 'admin123456'), + 'port': int(os.getenv('ROGDB_PORT', 5432)) +} + +# PostgreSQL予約語をクォートで囲む必要があるカラム +RESERVED_KEYWORDS = { + 'like', 'order', 'group', 'user', 'table', 'where', 'select', 'insert', + 'update', 'delete', 'create', 'drop', 'alter', 'index', 'constraint' +} + +class RogTableMigrator: + """Rogテーブル移行クラス (エラー修正版)""" + + def __init__(self): + self.old_conn = None + self.new_conn = None + self.old_cursor = None + self.new_cursor = None + self.migration_stats = {} + + def quote_column_if_needed(self, column_name): + """予約語やキャメルケースの場合はダブルクォートで囲む""" + # 予約語チェック + if column_name.lower() in RESERVED_KEYWORDS: + return f'"{column_name}"' + + # キャメルケースや大文字が含まれる場合もクォート + if any(c.isupper() for c in column_name) or column_name != column_name.lower(): + return f'"{column_name}"' + + return column_name + + def connect_databases(self): + """データベース接続""" + try: + logger.info("データベースに接続中...") + self.old_conn = psycopg2.connect(**OLD_ROGDB_CONFIG) + self.new_conn = psycopg2.connect(**ROGDB_CONFIG) + + # autocommitを有効にしてトランザクション問題を回避 + self.old_conn.autocommit = True + self.new_conn.autocommit = False # 新しい方は手動コミット + + self.old_cursor = self.old_conn.cursor() + self.new_cursor = self.new_conn.cursor() + + logger.info("✅ データベース接続成功") + return True + + except Exception as e: + logger.error(f"❌ データベース接続エラー: {e}") + return False + + def close_connections(self): + """データベース接続クローズ""" + try: + if self.old_cursor: + self.old_cursor.close() + if self.new_cursor: + self.new_cursor.close() + if self.old_conn: + self.old_conn.close() + if self.new_conn: + self.new_conn.close() + logger.info("データベース接続をクローズしました") + except Exception as e: + logger.warning(f"接続クローズ時の警告: {e}") + + def get_rog_tables(self): + """rog_で始まるテーブル一覧を取得""" + try: + # old_rogdbのrog_テーブル一覧 + self.old_cursor.execute(""" + SELECT table_name + FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name LIKE 'rog_%' + ORDER BY table_name + """) + old_tables = [row[0] for row in self.old_cursor.fetchall()] + + # rogdbのrog_テーブル一覧 + self.new_cursor.execute(""" + SELECT table_name + FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name LIKE 'rog_%' + ORDER BY table_name + """) + new_tables = [row[0] for row in self.new_cursor.fetchall()] + + # 共通テーブル + common_tables = list(set(old_tables) & set(new_tables)) + + logger.info(f"old_rogdb rog_テーブル: {len(old_tables)}個") + logger.info(f"rogdb rog_テーブル: {len(new_tables)}個") + logger.info(f"共通 rog_テーブル: {len(common_tables)}個") + + return old_tables, new_tables, common_tables + + except Exception as e: + logger.error(f"テーブル一覧取得エラー: {e}") + return [], [], [] + + def get_table_structure(self, table_name, cursor): + """テーブル構造を取得 (エラーハンドリング強化)""" + try: + cursor.execute(""" + SELECT column_name, data_type, is_nullable, column_default + FROM information_schema.columns + WHERE table_name = %s + AND table_schema = 'public' + ORDER BY ordinal_position + """, (table_name,)) + + columns = cursor.fetchall() + return { + 'columns': [col[0] for col in columns], + 'details': columns + } + except Exception as e: + logger.error(f"テーブル構造取得エラー ({table_name}): {e}") + # トランザクションエラーを回避 + try: + cursor.connection.rollback() + except: + pass + return {'columns': [], 'details': []} + + def get_primary_key(self, table_name, cursor): + """主キーカラムを取得""" + try: + cursor.execute(""" + SELECT a.attname + FROM pg_index i + JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) + WHERE i.indrelid = %s::regclass AND i.indisprimary + """, (table_name,)) + + pk_columns = [row[0] for row in cursor.fetchall()] + return pk_columns if pk_columns else ['id'] + + except Exception as e: + logger.warning(f"主キー取得エラー ({table_name}): {e}") + return ['id'] # デフォルトでidを使用 + + def migrate_table_data(self, table_name): + """個別テーブルのデータ移行 (エラー修正版)""" + logger.info(f"\n=== {table_name} データ移行開始 ===") + + try: + # テーブル構造比較 + old_structure = self.get_table_structure(table_name, self.old_cursor) + new_structure = self.get_table_structure(table_name, self.new_cursor) + + old_columns = set(old_structure['columns']) + new_columns = set(new_structure['columns']) + common_columns = old_columns & new_columns + + if not common_columns: + logger.warning(f"⚠️ {table_name}: 共通カラムがありません") + return {'inserted': 0, 'updated': 0, 'errors': 0} + + logger.info(f"共通カラム ({len(common_columns)}個): {sorted(common_columns)}") + + # 主キー取得 + pk_columns = self.get_primary_key(table_name, self.new_cursor) + logger.info(f"主キー: {pk_columns}") + + # カラム名を予約語対応でクォート + quoted_columns = [self.quote_column_if_needed(col) for col in common_columns] + columns_str = ', '.join(quoted_columns) + + # old_rogdbからデータ取得 + try: + self.old_cursor.execute(f"SELECT {columns_str} FROM {table_name}") + old_records = self.old_cursor.fetchall() + except Exception as e: + logger.error(f"❌ {table_name} データ取得エラー: {e}") + return {'inserted': 0, 'updated': 0, 'errors': 1} + + if not old_records: + logger.info(f"✅ {table_name}: 移行対象データなし") + return {'inserted': 0, 'updated': 0, 'errors': 0} + + logger.info(f"移行対象レコード数: {len(old_records)}件") + + # 統計情報 + inserted_count = 0 + updated_count = 0 + error_count = 0 + + # レコード移行処理 + for i, record in enumerate(old_records): + try: + # レコードデータを辞書形式に変換 + record_dict = dict(zip(common_columns, record)) + + # 主キー値を取得 + pk_values = [] + pk_conditions = [] + for pk_col in pk_columns: + if pk_col in record_dict: + pk_values.append(record_dict[pk_col]) + quoted_pk_col = self.quote_column_if_needed(pk_col) + pk_conditions.append(f"{quoted_pk_col} = %s") + + if not pk_values: + error_count += 1 + continue + + # 既存レコード確認 + check_query = f"SELECT COUNT(*) FROM {table_name} WHERE {' AND '.join(pk_conditions)}" + self.new_cursor.execute(check_query, pk_values) + exists = self.new_cursor.fetchone()[0] > 0 + + if exists: + # UPDATE処理 + set_clauses = [] + update_values = [] + + for col in common_columns: + if col not in pk_columns: # 主キー以外を更新 + quoted_col = self.quote_column_if_needed(col) + set_clauses.append(f"{quoted_col} = %s") + update_values.append(record_dict[col]) + + if set_clauses: + update_query = f""" + UPDATE {table_name} + SET {', '.join(set_clauses)} + WHERE {' AND '.join(pk_conditions)} + """ + self.new_cursor.execute(update_query, update_values + pk_values) + updated_count += 1 + + else: + # INSERT処理 + insert_columns = list(common_columns) + insert_values = [record_dict[col] for col in insert_columns] + quoted_insert_columns = [self.quote_column_if_needed(col) for col in insert_columns] + placeholders = ', '.join(['%s'] * len(insert_columns)) + + insert_query = f""" + INSERT INTO {table_name} ({', '.join(quoted_insert_columns)}) + VALUES ({placeholders}) + """ + self.new_cursor.execute(insert_query, insert_values) + inserted_count += 1 + + # 定期的なコミット + if (i + 1) % 100 == 0: + self.new_conn.commit() + logger.info(f" 進捗: {i + 1}/{len(old_records)} 件処理完了") + + except Exception as e: + error_count += 1 + logger.error(f" レコード処理エラー (行{i+1}): {e}") + # トランザクションロールバック + try: + self.new_conn.rollback() + except: + pass + + if error_count > 10: # エラー上限 + logger.error(f"❌ {table_name}: エラー数が上限を超えました") + break + + # 最終コミット + try: + self.new_conn.commit() + except Exception as e: + logger.error(f"コミットエラー: {e}") + + # 結果サマリー + stats = { + 'inserted': inserted_count, + 'updated': updated_count, + 'errors': error_count, + 'total': len(old_records) + } + + logger.info(f"✅ {table_name} 移行完了:") + logger.info(f" 挿入: {inserted_count}件") + logger.info(f" 更新: {updated_count}件") + logger.info(f" エラー: {error_count}件") + + self.migration_stats[table_name] = stats + return stats + + except Exception as e: + logger.error(f"❌ {table_name} 移行エラー: {e}") + try: + self.new_conn.rollback() + except: + pass + return {'inserted': 0, 'updated': 0, 'errors': 1} + + def run_migration(self, exclude_tables=None): + """全体移行実行""" + if exclude_tables is None: + exclude_tables = [] + + logger.info("=" * 80) + logger.info("Old RogDB → RogDB データ移行開始") + logger.info("=" * 80) + + try: + # データベース接続 + if not self.connect_databases(): + return False + + # テーブル一覧取得 + old_tables, new_tables, common_tables = self.get_rog_tables() + + if not common_tables: + logger.error("❌ 移行対象テーブルがありません") + return False + + # 除外テーブルをフィルタリング + migration_tables = [t for t in common_tables if t not in exclude_tables] + logger.info(f"移行対象テーブル ({len(migration_tables)}個): {migration_tables}") + + # 各テーブルを移行 + total_inserted = 0 + total_updated = 0 + total_errors = 0 + + for table_name in migration_tables: + stats = self.migrate_table_data(table_name) + total_inserted += stats['inserted'] + total_updated += stats['updated'] + total_errors += stats['errors'] + + # 全体サマリー出力 + logger.info("=" * 80) + logger.info("移行完了サマリー") + logger.info("=" * 80) + logger.info(f"処理対象テーブル: {len(migration_tables)}個") + logger.info(f"総挿入件数: {total_inserted}件") + logger.info(f"総更新件数: {total_updated}件") + logger.info(f"総エラー件数: {total_errors}件") + + logger.info("\n--- テーブル別詳細 ---") + for table_name, stats in self.migration_stats.items(): + logger.info(f"{table_name}: 挿入{stats['inserted']}, 更新{stats['updated']}, エラー{stats['errors']}") + + if total_errors > 0: + logger.warning(f"⚠️ {total_errors}件のエラーがありました") + else: + logger.info("✅ 全ての移行が正常に完了しました!") + + return True + + except Exception as e: + logger.error(f"❌ 移行処理エラー: {e}") + return False + + finally: + self.close_connections() + +def main(): + """メイン処理""" + logger.info("Old RogDB → RogDB データ移行スクリプト") + + # 除外テーブル設定 + exclude_tables = [] + if os.getenv('EXCLUDE_TABLES'): + exclude_tables = [t.strip() for t in os.getenv('EXCLUDE_TABLES').split(',')] + logger.info(f"除外テーブル: {exclude_tables}") + + # 移行実行 + migrator = RogTableMigrator() + success = migrator.run_migration(exclude_tables=exclude_tables) + + if success: + logger.info("移行処理が完了しました") + sys.exit(0) + else: + logger.error("移行処理が失敗しました") + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/migration_data_protection.py b/migration_data_protection.py index a1561d8..4f63c99 100644 --- a/migration_data_protection.py +++ b/migration_data_protection.py @@ -107,19 +107,19 @@ def check_database_connectivity(): # rogdb DB接続確認 target_conn = psycopg2.connect(**ROGDB_DB) target_cursor = target_conn.cursor() - target_cursor.execute("SELECT COUNT(*) FROM rog_gpscheckin") + target_cursor.execute("SELECT COUNT(*) FROM gps_checkins") target_count = target_cursor.fetchone()[0] - print(f"✅ rogdb DB接続成功: rog_gpscheckin {target_count}件") + print(f"✅ rogdb DB接続成功: gps_checkins {target_count}件") # 移行先テーブル構造確認 target_cursor.execute(""" SELECT column_name, data_type FROM information_schema.columns - WHERE table_name = 'rog_gpscheckin' + WHERE table_name = 'gps_checkins' ORDER BY ordinal_position """) target_columns = target_cursor.fetchall() - print("📋 rog_gpscheckinテーブル構造:") + print("📋 gps_checkinsテーブル構造:") for col_name, col_type in target_columns: print(f" {col_name}: {col_type}") @@ -212,7 +212,7 @@ def backup_existing_data(target_cursor): target_cursor.execute("SELECT COUNT(*) FROM rog_member") member_count = target_cursor.fetchone()[0] - target_cursor.execute("SELECT COUNT(*) FROM rog_gpscheckin") + target_cursor.execute("SELECT COUNT(*) FROM gps_checkins") checkin_count = target_cursor.fetchone()[0] # Location2025データ数も確認 @@ -228,7 +228,7 @@ def backup_existing_data(target_cursor): print(f" rog_entry: {entry_count} 件 (保護対象)") print(f" rog_team: {team_count} 件 (保護対象)") print(f" rog_member: {member_count} 件 (保護対象)") - print(f" rog_gpscheckin: {checkin_count} 件 (移行対象)") + print(f" gps_checkins: {checkin_count} 件 (移行対象)") if entry_count > 0 or team_count > 0 or member_count > 0: print("✅ 既存のcore application dataが検出されました。これらは保護されます。") @@ -265,7 +265,7 @@ def migrate_gps_data(source_cursor, target_cursor): target_cursor.execute(""" SELECT column_name FROM information_schema.columns - WHERE table_name = 'rog_gpscheckin' + WHERE table_name = 'gps_checkins' ORDER BY ordinal_position """) target_columns = [row[0] for row in target_cursor.fetchall()] @@ -370,7 +370,7 @@ def migrate_gps_data(source_cursor, target_cursor): columns_str = ', '.join(final_columns) target_cursor.execute(f""" - INSERT INTO rog_gpscheckin ({columns_str}) + INSERT INTO gps_checkins ({columns_str}) VALUES ({placeholders}) """, final_values) @@ -406,7 +406,7 @@ def main(): print("=" * 60) print("GPS記録データ移行スクリプト (既存データ保護版)") print("=" * 60) - print("移行対象: gifuroge.gps_information → rogdb.rog_gpscheckin") + print("移行対象: gifuroge.gps_information → rogdb.gps_checkins") print("既存データ保護: rog_entry, rog_team, rog_member, rog_location2025") print("=" * 60) diff --git a/migration_location2025_support.py b/migration_location2025_support.py index 7c0a1f7..8c90ec1 100644 --- a/migration_location2025_support.py +++ b/migration_location2025_support.py @@ -354,21 +354,21 @@ def main(): print("=== Location2025対応版移行プログラム開始 ===") print("注意: 既存のentry、team、member、location2025データは削除されません") - # データベース接続設定 + # データベース接続設定(環境変数から取得、デフォルト値あり) source_config = { - 'host': 'localhost', - 'port': '5433', - 'database': 'gifuroge', - 'user': 'postgres', - 'password': 'postgres' + 'host': os.getenv('SOURCE_DB_HOST', 'postgres-db'), + 'port': os.getenv('SOURCE_DB_PORT', '5432'), + 'database': os.getenv('SOURCE_DB_NAME', 'gifuroge'), + 'user': os.getenv('SOURCE_DB_USER', 'admin'), + 'password': os.getenv('SOURCE_DB_PASSWORD', 'admin123456') } target_config = { - 'host': 'localhost', - 'port': '5432', - 'database': 'rogdb', - 'user': 'postgres', - 'password': 'postgres' + 'host': os.getenv('TARGET_DB_HOST', 'postgres-db'), + 'port': os.getenv('TARGET_DB_PORT', '5432'), + 'database': os.getenv('TARGET_DB_NAME', 'rogdb'), + 'user': os.getenv('TARGET_DB_USER', 'admin'), + 'password': os.getenv('TARGET_DB_PASSWORD', 'admin123456') } source_conn = None diff --git a/migration_statistics.py b/migration_statistics.py new file mode 100644 index 0000000..fe37dba --- /dev/null +++ b/migration_statistics.py @@ -0,0 +1,406 @@ +#!/usr/bin/env python3 +""" +移行結果統計情報表示スクリプト +Docker Compose環境で実行可能 +""" + +import os +import sys +import psycopg2 +from datetime import datetime, timedelta +import pytz +from collections import defaultdict +import json + +def connect_database(): + """データベースに接続""" + try: + conn = psycopg2.connect( + host=os.environ.get('DB_HOST', 'postgres-db'), + port=os.environ.get('DB_PORT', '5432'), + database=os.environ.get('POSTGRES_DBNAME', 'rogdb'), + user=os.environ.get('POSTGRES_USER', 'admin'), + password=os.environ.get('POSTGRES_PASS', 'admin123456') + ) + return conn + except Exception as e: + print(f"❌ データベース接続エラー: {e}") + return None + +def get_basic_statistics(cursor): + """基本統計情報を取得""" + print("\n" + "="*80) + print("📊 移行データ基本統計情報") + print("="*80) + + # テーブル別レコード数 + tables = [ + ('rog_newevent2', 'イベント'), + ('rog_team', 'チーム'), + ('rog_member', 'メンバー'), + ('rog_entry', 'エントリー'), + ('rog_gpscheckin', 'GPSチェックイン'), + ('rog_checkpoint', 'チェックポイント'), + ('rog_location2025', 'ロケーション2025'), + ('rog_customuser', 'ユーザー') + ] + + print("\n📋 テーブル別レコード数:") + print("テーブル名 日本語名 レコード数") + print("-" * 65) + + total_records = 0 + for table_name, japanese_name in tables: + try: + cursor.execute(f"SELECT COUNT(*) FROM {table_name}") + count = cursor.fetchone()[0] + total_records += count + print(f"{table_name:<25} {japanese_name:<15} {count:>10,}件") + except Exception as e: + print(f"{table_name:<25} {japanese_name:<15} {'エラー':>10}") + + print("-" * 65) + print(f"{'合計':<41} {total_records:>10,}件") + +def get_event_statistics(cursor): + """イベント別統計情報""" + print("\n" + "="*80) + print("🎯 イベント別統計情報") + print("="*80) + + # イベント一覧と基本情報 + cursor.execute(""" + SELECT id, event_name, event_date, created_at + FROM rog_newevent2 + ORDER BY event_date DESC + """) + + events = cursor.fetchall() + if not events: + print("イベントデータがありません") + return + + print(f"\n📅 登録イベント数: {len(events)}件") + print("\nイベント詳細:") + print("ID イベント名 開催日 登録日時") + print("-" * 60) + + for event in events: + event_id, event_name, event_date, created_at = event + event_date_str = event_date.strftime("%Y-%m-%d") if event_date else "未設定" + created_str = created_at.strftime("%Y-%m-%d %H:%M") if created_at else "未設定" + print(f"{event_id:>2} {event_name:<15} {event_date_str} {created_str}") + + # イベント別参加統計 + cursor.execute(""" + SELECT + e.event_name, + COUNT(DISTINCT t.id) as team_count, + COUNT(DISTINCT m.id) as member_count, + COUNT(DISTINCT en.id) as entry_count + FROM rog_newevent2 e + LEFT JOIN rog_team t ON e.id = t.event_id + LEFT JOIN rog_member m ON t.id = m.team_id + LEFT JOIN rog_entry en ON t.id = en.team_id + GROUP BY e.id, e.event_name + ORDER BY team_count DESC + """) + + participation_stats = cursor.fetchall() + + print("\n👥 イベント別参加統計:") + print("イベント名 チーム数 メンバー数 エントリー数") + print("-" * 55) + + total_teams = 0 + total_members = 0 + total_entries = 0 + + for stat in participation_stats: + event_name, team_count, member_count, entry_count = stat + total_teams += team_count + total_members += member_count + total_entries += entry_count + print(f"{event_name:<15} {team_count:>6}件 {member_count:>8}件 {entry_count:>9}件") + + print("-" * 55) + print(f"{'合計':<15} {total_teams:>6}件 {total_members:>8}件 {total_entries:>9}件") + +def get_gps_checkin_statistics(cursor): + """GPSチェックイン統計""" + print("\n" + "="*80) + print("📍 GPSチェックイン統計情報") + print("="*80) + + # 基本統計 + cursor.execute(""" + SELECT + COUNT(*) as total_checkins, + COUNT(DISTINCT zekken) as unique_teams, + COUNT(DISTINCT cp_number) as unique_checkpoints, + MIN(checkin_time) as earliest_checkin, + MAX(checkin_time) as latest_checkin + FROM rog_gpscheckin + """) + + basic_stats = cursor.fetchone() + if not basic_stats or basic_stats[0] == 0: + print("GPSチェックインデータがありません") + return + + total_checkins, unique_teams, unique_checkpoints, earliest, latest = basic_stats + + print(f"\n📊 基本統計:") + print(f"総チェックイン数: {total_checkins:,}件") + print(f"参加チーム数: {unique_teams:,}チーム") + print(f"利用CP数: {unique_checkpoints:,}箇所") + + if earliest and latest: + print(f"最早チェックイン: {earliest.strftime('%Y-%m-%d %H:%M:%S')}") + print(f"最終チェックイン: {latest.strftime('%Y-%m-%d %H:%M:%S')}") + + # 時間帯別分析 + cursor.execute(""" + SELECT + EXTRACT(hour FROM checkin_time) as hour, + COUNT(*) as count + FROM rog_gpscheckin + GROUP BY EXTRACT(hour FROM checkin_time) + ORDER BY hour + """) + + hourly_stats = cursor.fetchall() + + print(f"\n⏰ 時間帯別チェックイン分布:") + print("時間 チェックイン数 グラフ") + print("-" * 40) + + max_count = max([count for _, count in hourly_stats]) if hourly_stats else 1 + + for hour, count in hourly_stats: + bar_length = int((count / max_count) * 20) + bar = "█" * bar_length + print(f"{int(hour):>2}時 {count:>8}件 {bar}") + + # CP別利用統計(上位10位) + cursor.execute(""" + SELECT + cp_number, + COUNT(*) as checkin_count, + COUNT(DISTINCT zekken) as team_count + FROM rog_gpscheckin + GROUP BY cp_number + ORDER BY checkin_count DESC + LIMIT 10 + """) + + cp_stats = cursor.fetchall() + + print(f"\n🏅 CP利用ランキング(上位10位):") + print("順位 CP番号 チェックイン数 利用チーム数") + print("-" * 40) + + for i, (cp_number, checkin_count, team_count) in enumerate(cp_stats, 1): + print(f"{i:>2}位 CP{cp_number:>3} {checkin_count:>8}件 {team_count:>7}チーム") + +def get_team_statistics(cursor): + """チーム統計""" + print("\n" + "="*80) + print("👥 チーム統計情報") + print("="*80) + + # チーム基本統計 + cursor.execute(""" + SELECT + COUNT(*) as total_teams, + COUNT(DISTINCT class_name) as unique_classes, + AVG(CASE WHEN member_count > 0 THEN member_count END) as avg_members + FROM ( + SELECT + t.id, + t.class_name, + COUNT(m.id) as member_count + FROM rog_team t + LEFT JOIN rog_member m ON t.id = m.team_id + GROUP BY t.id, t.class_name + ) team_stats + """) + + team_basic = cursor.fetchone() + total_teams, unique_classes, avg_members = team_basic + + print(f"\n📊 基本統計:") + print(f"総チーム数: {total_teams:,}チーム") + print(f"クラス数: {unique_classes or 0}種類") + print(f"平均メンバー数: {avg_members:.1f}人/チーム" if avg_members else "平均メンバー数: データなし") + + # クラス別統計 + cursor.execute(""" + SELECT + COALESCE(class_name, '未分類') as class_name, + COUNT(*) as team_count, + COUNT(CASE WHEN member_count > 0 THEN 1 END) as active_teams + FROM ( + SELECT + t.class_name, + COUNT(m.id) as member_count + FROM rog_team t + LEFT JOIN rog_member m ON t.id = m.team_id + GROUP BY t.id, t.class_name + ) team_stats + GROUP BY class_name + ORDER BY team_count DESC + """) + + class_stats = cursor.fetchall() + + if class_stats: + print(f"\n🏆 クラス別チーム数:") + print("クラス名 チーム数 アクティブ") + print("-" * 35) + + for class_name, team_count, active_teams in class_stats: + print(f"{class_name:<15} {team_count:>6}件 {active_teams:>7}件") + +def get_data_quality_check(cursor): + """データ品質チェック""" + print("\n" + "="*80) + print("🔍 データ品質チェック") + print("="*80) + + checks = [] + + # 1. 重複チェック + cursor.execute(""" + SELECT COUNT(*) FROM ( + SELECT zekken, cp_number, checkin_time, COUNT(*) + FROM rog_gpscheckin + GROUP BY zekken, cp_number, checkin_time + HAVING COUNT(*) > 1 + ) duplicates + """) + duplicate_count = cursor.fetchone()[0] + checks.append(("重複チェックイン", duplicate_count, "件")) + + # 2. 異常時刻チェック(0時台) + cursor.execute(""" + SELECT COUNT(*) FROM rog_gpscheckin + WHERE EXTRACT(hour FROM checkin_time) = 0 + """) + zero_hour_count = cursor.fetchone()[0] + checks.append(("0時台チェックイン", zero_hour_count, "件")) + + # 3. 未来日時チェック + cursor.execute(""" + SELECT COUNT(*) FROM rog_gpscheckin + WHERE checkin_time > NOW() + """) + future_count = cursor.fetchone()[0] + checks.append(("未来日時チェックイン", future_count, "件")) + + # 4. メンバー不在チーム + cursor.execute(""" + SELECT COUNT(*) FROM rog_team t + LEFT JOIN rog_member m ON t.id = m.team_id + WHERE m.id IS NULL + """) + no_member_teams = cursor.fetchone()[0] + checks.append(("メンバー不在チーム", no_member_teams, "チーム")) + + # 5. エントリー不在チーム + cursor.execute(""" + SELECT COUNT(*) FROM rog_team t + LEFT JOIN rog_entry e ON t.id = e.team_id + WHERE e.id IS NULL + """) + no_entry_teams = cursor.fetchone()[0] + checks.append(("エントリー不在チーム", no_entry_teams, "チーム")) + + print("\n🧪 品質チェック結果:") + print("チェック項目 件数 状態") + print("-" * 40) + + for check_name, count, unit in checks: + status = "✅ 正常" if count == 0 else "⚠️ 要確認" + print(f"{check_name:<15} {count:>6}{unit} {status}") + +def export_statistics_json(cursor): + """統計情報をJSONで出力""" + print("\n" + "="*80) + print("📄 統計情報JSON出力") + print("="*80) + + statistics = {} + + # 基本統計 + cursor.execute("SELECT COUNT(*) FROM rog_gpscheckin") + statistics['total_checkins'] = cursor.fetchone()[0] + + cursor.execute("SELECT COUNT(DISTINCT zekken) FROM rog_gpscheckin") + statistics['unique_teams'] = cursor.fetchone()[0] + + cursor.execute("SELECT COUNT(*) FROM rog_newevent2") + statistics['total_events'] = cursor.fetchone()[0] + + # イベント別統計 + cursor.execute(""" + SELECT event_name, COUNT(*) as checkin_count + FROM rog_newevent2 e + LEFT JOIN rog_team t ON e.id = t.event_id + LEFT JOIN rog_gpscheckin g ON t.zekken = g.zekken + GROUP BY e.id, event_name + ORDER BY checkin_count DESC + """) + + event_stats = {} + for event_name, count in cursor.fetchall(): + event_stats[event_name] = count + + statistics['event_checkins'] = event_stats + statistics['generated_at'] = datetime.now().isoformat() + + # ファイル出力 + output_file = f"/app/migration_statistics_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" + + try: + with open(output_file, 'w', encoding='utf-8') as f: + json.dump(statistics, f, ensure_ascii=False, indent=2) + print(f"✅ 統計情報をJSONで出力しました: {output_file}") + except Exception as e: + print(f"❌ JSON出力エラー: {e}") + +def main(): + """メイン処理""" + print("🚀 移行結果統計情報表示スクリプト開始") + print(f"実行時刻: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + + # データベース接続 + conn = connect_database() + if not conn: + sys.exit(1) + + try: + cursor = conn.cursor() + + # 各統計情報を表示 + get_basic_statistics(cursor) + get_event_statistics(cursor) + get_gps_checkin_statistics(cursor) + get_team_statistics(cursor) + get_data_quality_check(cursor) + export_statistics_json(cursor) + + print("\n" + "="*80) + print("✅ 統計情報表示完了") + print("="*80) + + except Exception as e: + print(f"❌ 統計処理中にエラーが発生しました: {e}") + import traceback + traceback.print_exc() + finally: + cursor.close() + conn.close() + +if __name__ == "__main__": + main()