|
@@ -0,0 +1,120 @@
|
|
|
+import sqlite3
|
|
|
+import os
|
|
|
+import logging
|
|
|
+
|
|
|
+# Configure logging
|
|
|
+logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
|
|
+logger = logging.getLogger(__name__)
|
|
|
+
|
|
|
+# Determine the absolute path to the project root directory
|
|
|
+# Adjusted for the script being in src/migrations/
|
|
|
+PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
+DB_PATH = os.path.join(PROJECT_ROOT, "data", "trading_stats.sqlite")
|
|
|
+
|
|
|
+# MIGRATION_SET_VERSION identifies this specific group of schema changes.
|
|
|
+# We are bringing the schema to a state that includes the new P&L/risk columns.
|
|
|
+MIGRATION_SET_VERSION = 1
|
|
|
+
|
|
|
+def _get_db_connection(db_path):
|
|
|
+ """Gets a database connection."""
|
|
|
+ return sqlite3.connect(db_path)
|
|
|
+
|
|
|
+def _get_current_schema_version(conn):
|
|
|
+ """Retrieves the current schema version from the metadata table."""
|
|
|
+ try:
|
|
|
+ cursor = conn.cursor()
|
|
|
+ # Ensure metadata table exists (idempotent)
|
|
|
+ cursor.execute("CREATE TABLE IF NOT EXISTS metadata (key TEXT PRIMARY KEY, value TEXT)")
|
|
|
+ conn.commit()
|
|
|
+
|
|
|
+ cursor.execute("SELECT value FROM metadata WHERE key = 'schema_version'")
|
|
|
+ row = cursor.fetchone()
|
|
|
+ return int(row[0]) if row else 0 # Default to 0 if no version is set
|
|
|
+ except sqlite3.Error as e:
|
|
|
+ logger.error(f"Error getting schema version: {e}. Assuming version 0.")
|
|
|
+ return 0
|
|
|
+
|
|
|
+def _set_schema_version(conn, version):
|
|
|
+ """Sets the schema version in the metadata table."""
|
|
|
+ try:
|
|
|
+ cursor = conn.cursor()
|
|
|
+ cursor.execute("INSERT OR REPLACE INTO metadata (key, value) VALUES ('schema_version', ?)", (str(version),))
|
|
|
+ conn.commit()
|
|
|
+ logger.info(f"Database schema version successfully set to {version}.")
|
|
|
+ except sqlite3.Error as e:
|
|
|
+ logger.error(f"Error setting schema version to {version}: {e}")
|
|
|
+
|
|
|
+def _column_exists(conn, table_name, column_name):
|
|
|
+ """Checks if a column exists in a table using PRAGMA table_info."""
|
|
|
+ cursor = conn.cursor()
|
|
|
+ cursor.execute(f"PRAGMA table_info({table_name})")
|
|
|
+ columns = [row[1] for row in cursor.fetchall()]
|
|
|
+ return column_name in columns
|
|
|
+
|
|
|
+def _add_column_if_not_exists(conn, table_name, column_name, column_definition):
|
|
|
+ """Adds a column to a table if it doesn't already exist."""
|
|
|
+ if not _column_exists(conn, table_name, column_name):
|
|
|
+ try:
|
|
|
+ cursor = conn.cursor()
|
|
|
+ query = f"ALTER TABLE {table_name} ADD COLUMN {column_name} {column_definition}"
|
|
|
+ cursor.execute(query)
|
|
|
+ conn.commit()
|
|
|
+ logger.info(f"Successfully added column '{column_name}' to table '{table_name}'.")
|
|
|
+ except sqlite3.OperationalError as e:
|
|
|
+ # This specific error means the column might already exist (though _column_exists should prevent this)
|
|
|
+ # or some other operational issue.
|
|
|
+ if f"duplicate column name: {column_name}" in str(e).lower():
|
|
|
+ logger.info(f"Column '{column_name}' effectively already exists in '{table_name}'. No action needed.")
|
|
|
+ else:
|
|
|
+ logger.error(f"Error adding column '{column_name}' to table '{table_name}': {e}")
|
|
|
+ else:
|
|
|
+ logger.info(f"Column '{column_name}' already exists in table '{table_name}'. No action taken.")
|
|
|
+
|
|
|
+def run_migrations():
|
|
|
+ """Runs the database migrations."""
|
|
|
+ logger.info(f"Attempting to migrate database at: {DB_PATH}")
|
|
|
+ if not os.path.exists(DB_PATH):
|
|
|
+ logger.info(f"Database file not found at {DB_PATH}. Nothing to migrate. "
|
|
|
+ f"The application will create it with the latest schema on its next start.")
|
|
|
+ return
|
|
|
+
|
|
|
+ conn = None
|
|
|
+ try:
|
|
|
+ conn = _get_db_connection(DB_PATH)
|
|
|
+ current_db_version = _get_current_schema_version(conn)
|
|
|
+ logger.info(f"Current reported database schema version: {current_db_version}")
|
|
|
+
|
|
|
+ if current_db_version < MIGRATION_SET_VERSION:
|
|
|
+ logger.info(f"Schema version {current_db_version} is older than target migration set {MIGRATION_SET_VERSION}. Starting migration...")
|
|
|
+
|
|
|
+ # Define columns to add for MIGRATION_SET_VERSION 1
|
|
|
+ # These definitions match what's in the TradingStats._create_tables() method
|
|
|
+ columns_to_add = {
|
|
|
+ "liquidation_price": "REAL DEFAULT NULL",
|
|
|
+ "margin_used": "REAL DEFAULT NULL",
|
|
|
+ "leverage": "REAL DEFAULT NULL",
|
|
|
+ "position_value": "REAL DEFAULT NULL"
|
|
|
+ }
|
|
|
+
|
|
|
+ for col_name, col_def in columns_to_add.items():
|
|
|
+ _add_column_if_not_exists(conn, "trades", col_name, col_def)
|
|
|
+
|
|
|
+ # After all operations for this version are successful, update the schema version in DB
|
|
|
+ _set_schema_version(conn, MIGRATION_SET_VERSION)
|
|
|
+ logger.info(f"Successfully migrated database to schema version {MIGRATION_SET_VERSION}.")
|
|
|
+ else:
|
|
|
+ logger.info(f"Database schema version {current_db_version} is already at or newer than migration set {MIGRATION_SET_VERSION}. No migration needed for this set.")
|
|
|
+
|
|
|
+ except sqlite3.Error as e:
|
|
|
+ logger.error(f"A database error occurred during migration: {e}")
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"An unexpected error occurred during migration: {e}", exc_info=True)
|
|
|
+ finally:
|
|
|
+ if conn:
|
|
|
+ conn.close()
|
|
|
+ logger.info("Database connection closed.")
|
|
|
+
|
|
|
+if __name__ == "__main__":
|
|
|
+ logger.info("Starting database migration script...")
|
|
|
+ run_migrations()
|
|
|
+ logger.info("Database migration script finished.")
|