#!/bin/bash # Safe Database Migration Script # Usage: ./safe-migrate.sh [environment] # Environments: Development, Sandbox, Production, Oda set -e # Exit on any error ENVIRONMENT=${1:-"Development"} # Default to Development for safer initial testing TIMESTAMP=$(date +%Y%m%d_%H%M%S) BACKUP_DIR_NAME="backups" # Just the directory name LOGS_DIR_NAME="logs" # Just the directory name # Get the directory where the script is located SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" # Create logs directory first (before LOG_FILE is used) LOGS_DIR="$SCRIPT_DIR/$LOGS_DIR_NAME" mkdir -p "$LOGS_DIR" || { echo "Failed to create logs directory: $LOGS_DIR"; exit 1; } LOG_FILE="$SCRIPT_DIR/logs/migration_${ENVIRONMENT}_${TIMESTAMP}.log" # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Logging function log() { echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')] $1${NC}" | tee -a "$LOG_FILE" } warn() { echo -e "${YELLOW}[$(date +'%Y-%m-%d %H:%M:%S')] WARNING: $1${NC}" | tee -a "$LOG_FILE" } error() { echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: $1${NC}" | tee -a "$LOG_FILE" exit 1 } info() { echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')] INFO: $1${NC}" | tee -a "$LOG_FILE" } # --- Determine Base Paths --- # Get the directory where the script is located SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" log "Script is located in: $SCRIPT_DIR" # Define absolute paths for projects and common directories relative to the script # Assuming the project structure is: # your_repo/ # ├── scripts/safe-migrate.sh # └── src/ # ├── Managing.Api/ # ├── Managing.Infrastructure.Database/ # └── Managing.Docker/ PROJECT_ROOT_DIR="$(dirname "$SCRIPT_DIR")" # One level up from scripts/ SRC_DIR="$PROJECT_ROOT_DIR/src" DB_PROJECT_PATH="$SRC_DIR/Managing.Infrastructure.Database" API_PROJECT_PATH="$SRC_DIR/Managing.Api" DOCKER_DIR="$SRC_DIR/Managing.Docker" # Adjust if your docker-compose files are elsewhere # Define absolute path for backup directory with environment subfolder BACKUP_DIR="$SCRIPT_DIR/$BACKUP_DIR_NAME/$ENVIRONMENT" # --- Pre-checks and Setup --- info "Pre-flight checks..." command -v dotnet >/dev/null 2>&1 || error ".NET SDK is not installed. Please install .NET SDK to run this script." command -v docker >/dev/null 2>&1 || warn "Docker is not installed. This is fine if not running Development or Oda environment with Docker." command -v psql >/dev/null 2>&1 || warn "PostgreSQL CLI (psql) is not installed. Database connectivity checks will be skipped." command -v pg_dump >/dev/null 2>&1 || warn "PostgreSQL pg_dump is not installed. Will use EF Core migration script for backup instead." # Create backup directory (with environment subfolder) mkdir -p "$BACKUP_DIR" || error "Failed to create backup directory: $BACKUP_DIR" log "Backup directory created/verified: $BACKUP_DIR" log "🚀 Starting safe migration for environment: $ENVIRONMENT" # Validate environment case $ENVIRONMENT in "Development"|"Sandbox"|"Production"|"Oda") log "✅ Environment '$ENVIRONMENT' is valid" ;; *) error "❌ Invalid environment '$ENVIRONMENT'. Use: Development, Sandbox, Production, or Oda" ;; esac # Helper function to start PostgreSQL for Development (if still using Docker Compose) start_postgres_if_needed() { if [ "$ENVIRONMENT" = "Development" ] || [ "$ENVIRONMENT" = "Oda" ]; then # Assuming Oda also uses local Docker log "🔍 Checking if PostgreSQL is running for $ENVIRONMENT..." if ! docker ps --filter "name=postgres" --format "{{.Names}}" | grep -q "postgres"; then log "🐳 Starting PostgreSQL container for $ENVIRONMENT from $DOCKER_DIR..." # Execute docker-compose from the DOCKER_DIR (cd "$DOCKER_DIR" && docker-compose -f docker-compose.yml -f docker-compose.local.yml up -d postgres) || error "Failed to start PostgreSQL container." log "⏳ Waiting for PostgreSQL to be ready (15 seconds)..." sleep 15 else log "✅ PostgreSQL container is already running." fi fi } # Helper function to extract connection details from appsettings extract_connection_details() { local appsettings_file="$API_PROJECT_PATH/appsettings.$ENVIRONMENT.json" local default_appsettings="$API_PROJECT_PATH/appsettings.json" # Try environment-specific file first, then default if [ -f "$appsettings_file" ]; then log "📋 Reading connection string from: appsettings.$ENVIRONMENT.json" # Look for PostgreSql.ConnectionString first, then fallback to ConnectionString CONNECTION_STRING=$(grep -A 3 '"PostgreSql"' "$appsettings_file" | grep -o '"ConnectionString": *"[^"]*"' | cut -d'"' -f4) if [ -z "$CONNECTION_STRING" ]; then CONNECTION_STRING=$(grep -o '"ConnectionString": *"[^"]*"' "$appsettings_file" | cut -d'"' -f4) fi elif [ -f "$default_appsettings" ]; then log "📋 Reading connection string from: appsettings.json (default)" # Look for PostgreSql.ConnectionString first, then fallback to ConnectionString CONNECTION_STRING=$(grep -A 3 '"PostgreSql"' "$default_appsettings" | grep -o '"ConnectionString": *"[^"]*"' | cut -d'"' -f4) if [ -z "$CONNECTION_STRING" ]; then CONNECTION_STRING=$(grep -o '"ConnectionString": *"[^"]*"' "$default_appsettings" | cut -d'"' -f4) fi else warn "⚠️ Could not find appsettings file for environment $ENVIRONMENT" return 1 fi if [ -z "$CONNECTION_STRING" ]; then error "❌ Could not extract connection string from appsettings file" return 1 fi log "📋 Found connection string: $CONNECTION_STRING" # Parse connection string DB_HOST=$(echo "$CONNECTION_STRING" | grep -o 'Host=[^;]*' | cut -d'=' -f2) DB_PORT=$(echo "$CONNECTION_STRING" | grep -o 'Port=[^;]*' | cut -d'=' -f2) DB_NAME=$(echo "$CONNECTION_STRING" | grep -o 'Database=[^;]*' | cut -d'=' -f2) DB_USER=$(echo "$CONNECTION_STRING" | grep -o 'Username=[^;]*' | cut -d'=' -f2) DB_PASSWORD=$(echo "$CONNECTION_STRING" | grep -o 'Password=[^;]*' | cut -d'=' -f2) # Set defaults if not found DB_HOST=${DB_HOST:-"localhost"} DB_PORT=${DB_PORT:-"5432"} DB_NAME=${DB_NAME:-"postgres"} DB_USER=${DB_USER:-"postgres"} DB_PASSWORD=${DB_PASSWORD:-"postgres"} log "📋 Extracted connection details: $DB_HOST:$DB_PORT/$DB_NAME (user: $DB_USER, password: $DB_PASSWORD)" } # Helper function to get the first migration name get_first_migration() { local first_migration=$(cd "$DB_PROJECT_PATH" && dotnet ef migrations list --no-build --startup-project "$API_PROJECT_PATH" | head -1 | awk '{print $1}') echo "$first_migration" } # Helper function to test PostgreSQL connectivity test_postgres_connectivity() { if ! command -v psql >/dev/null 2>&1; then warn "⚠️ psql not available, skipping PostgreSQL connectivity test" return 0 fi log "🔍 Testing PostgreSQL connectivity with psql..." # For remote servers or when target database might not exist, test with postgres database first local test_database="$DB_NAME" if [ "$TARGET_DB_EXISTS" = "false" ]; then test_database="postgres" log "🔍 Target database doesn't exist, testing connectivity with 'postgres' database..." fi # Test basic connectivity if PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$test_database" -c "SELECT version();" >/dev/null 2>&1; then log "✅ PostgreSQL connectivity test passed" # Get database info log "📊 Database Information:" DB_INFO=$(PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$test_database" -t -c " SELECT 'Database: ' || current_database() || ' (Size: ' || pg_size_pretty(pg_database_size(current_database())) || ')', 'PostgreSQL Version: ' || version(), 'Connection: ' || inet_server_addr() || ':' || inet_server_port() " 2>/dev/null | tr '\n' ' ') log " $DB_INFO" # Only check migrations if we're testing the actual target database if [ "$test_database" = "$DB_NAME" ]; then # Check if __EFMigrationsHistory table exists if PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c "\dt __EFMigrationsHistory" >/dev/null 2>&1; then log "✅ EF Core migrations history table exists" # Count applied migrations MIGRATION_COUNT=$(PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c "SELECT COUNT(*) FROM \"__EFMigrationsHistory\";" 2>/dev/null | tr -d ' ') log "📋 Applied migrations count: $MIGRATION_COUNT" # Show recent migrations if [ "$MIGRATION_COUNT" -gt 0 ]; then log "📋 Recent migrations:" RECENT_MIGRATIONS=$(PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c " SELECT \"MigrationId\" FROM \"__EFMigrationsHistory\" ORDER BY \"MigrationId\" DESC LIMIT 5; " 2>/dev/null | sed 's/^/ /') echo "$RECENT_MIGRATIONS" fi else warn "⚠️ EF Core migrations history table not found - database may be empty" fi else log "📋 Connectivity test completed using 'postgres' database (target database will be created)" fi return 0 else error "❌ PostgreSQL connectivity test failed" error " Host: $DB_HOST, Port: $DB_PORT, Database: $test_database, User: $DB_USER" return 1 fi } # --- Core Logic --- # No global 'cd' needed here. All paths are now absolute. # This makes the script much more robust to where it's executed from. # Set ASPNETCORE_ENVIRONMENT to load the correct appsettings export ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" log "ASPNETCORE_ENVIRONMENT set to: $ASPNETCORE_ENVIRONMENT" # If Development or Oda, start local PostgreSQL start_postgres_if_needed # Extract connection details from appsettings extract_connection_details # Step 0: Build the .NET projects to ensure they are up to date log "🔨 Step 0: Building .NET projects..." log "🔧 Building Managing.Infrastructure.Database project..." if (cd "$DB_PROJECT_PATH" && dotnet build); then log "✅ Managing.Infrastructure.Database project built successfully" else error "❌ Failed to build Managing.Infrastructure.Database project" fi # Step 1: Check Database Connection and Create if Needed log "🔧 Step 1: Checking database connection and creating database if needed..." # Log the environment and expected connection details (for user info, still relies on appsettings) log "🔧 Using environment: $ENVIRONMENT" log "📋 Connection details: $DB_HOST:$DB_PORT/$DB_NAME (user: $DB_USER)" # Initial connectivity check - test if we can reach the database server log "🔍 Step 1a: Testing basic database server connectivity..." if command -v psql >/dev/null 2>&1; then # Test if we can connect to the postgres database (which should always exist) log "🔍 Connecting to default 'postgres' database to verify server connectivity..." if PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d postgres -c "SELECT 1;" >/dev/null 2>&1; then log "✅ Database server connectivity test passed" # Check if our target database exists log "🔍 Checking if target database '$DB_NAME' exists..." DB_EXISTS=$(PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d postgres -t -c "SELECT 1 FROM pg_database WHERE datname = '$DB_NAME';" 2>/dev/null | tr -d ' ') if [ "$DB_EXISTS" = "1" ]; then log "✅ Target database '$DB_NAME' exists" TARGET_DB_EXISTS=true else log "⚠️ Target database '$DB_NAME' does not exist - will be created" TARGET_DB_EXISTS=false fi else error "❌ Database server connectivity test failed" error " Cannot reach PostgreSQL server at $DB_HOST:$DB_PORT with database 'postgres'" error " Please verify:" error " - Database server is running" error " - Network connectivity to $DB_HOST:$DB_PORT" error " - Credentials are correct (user: $DB_USER)" error " - Firewall settings allow connections" error " - The 'postgres' database exists (default PostgreSQL database)" fi else # Fallback: try to connect using EF Core to test basic connectivity log "🔄 psql not available, testing connectivity via EF Core..." if (cd "$DB_PROJECT_PATH" && dotnet ef migrations list --no-build --startup-project "$API_PROJECT_PATH" --connection "$CONNECTION_STRING") >/dev/null 2>&1; then log "✅ Database server connectivity test passed (via EF Core)" TARGET_DB_EXISTS=true # Assume it exists if EF Core can connect else warn "⚠️ Could not verify database server connectivity (psql not available)" warn " Proceeding with caution - connectivity will be tested during migration" TARGET_DB_EXISTS=false # Assume it doesn't exist if EF Core can't connect fi fi log "🔍 Step 1b: Testing database connection and checking if database exists via EF CLI..." # Test connection by listing migrations. If it fails, the database likely doesn't exist or is inaccessible. # Execute dotnet ef from DB_PROJECT_PATH for correct context, but pass API_PROJECT_PATH as startup. # Since we just built the projects, we can safely use --no-build flag for faster execution if (cd "$DB_PROJECT_PATH" && dotnet ef migrations list --no-build --startup-project "$API_PROJECT_PATH" --connection "$CONNECTION_STRING") >/dev/null 2>&1; then log "✅ EF Core database connection successful and database appears to exist." # Now test with psql for additional verification (this will use postgres db if target doesn't exist) test_postgres_connectivity # If psql connectivity test fails, stop the migration if [ $? -ne 0 ]; then error "❌ PostgreSQL connectivity test failed. Migration aborted for safety." error " Please verify your database connection and try again." fi else # Database doesn't exist or connection failed if [ "$TARGET_DB_EXISTS" = "false" ]; then log "📝 Database '$DB_NAME' does not exist. Creating database and applying migrations..." # Test connectivity with postgres database first (since target doesn't exist) test_postgres_connectivity # If connectivity test fails, stop the migration if [ $? -ne 0 ]; then error "❌ PostgreSQL connectivity test failed. Cannot proceed with database creation." error " Please verify your connection settings and try again." fi # Step 1: Create the database first log "🔧 Step 1: Creating database '$DB_NAME'..." if command -v psql >/dev/null 2>&1; then if PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d postgres -c "CREATE DATABASE \"$DB_NAME\";" >/dev/null 2>&1; then log "✅ Database '$DB_NAME' created successfully" else error "❌ Failed to create database '$DB_NAME'" error " Please verify you have sufficient privileges to create databases." fi else warn "⚠️ psql not available, attempting to create database via EF Core..." # EF Core will attempt to create the database during update fi # Step 2: Generate migration script for the new database log "📝 Step 2: Generating migration script for new database..." TEMP_MIGRATION_SCRIPT="$BACKUP_DIR/temp_migration_${ENVIRONMENT}_${TIMESTAMP}.sql" if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$TEMP_MIGRATION_SCRIPT"); then log "✅ Migration script generated successfully: $(basename "$TEMP_MIGRATION_SCRIPT")" # Step 3: Apply the migration script to the new database log "🔧 Step 3: Applying migration script to new database..." if command -v psql >/dev/null 2>&1; then if PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -f "$TEMP_MIGRATION_SCRIPT" >/dev/null 2>&1; then log "✅ Migration script applied successfully to new database" else error "❌ Failed to apply migration script to newly created database" fi else # Fallback to EF Core database update log "🔄 psql not available, using EF Core to apply migrations..." if (cd "$DB_PROJECT_PATH" && dotnet ef database update --no-build --startup-project "$API_PROJECT_PATH" --connection "$CONNECTION_STRING"); then log "✅ Database created and initialized successfully using EF Core" else ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && dotnet ef database update --no-build --startup-project "$API_PROJECT_PATH" --connection "$CONNECTION_STRING") 2>&1 || true ) error "❌ Failed to create and initialize database using EF Core." error " EF CLI Output: $ERROR_OUTPUT" fi fi # Clean up temporary migration script rm -f "$TEMP_MIGRATION_SCRIPT" else ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$TEMP_MIGRATION_SCRIPT") 2>&1 || true ) error "❌ Failed to generate migration script." error " EF CLI Output: $ERROR_OUTPUT" fi else warn "⚠️ Database connection failed but database may exist. Attempting to update existing database..." # Try to update the existing database if (cd "$DB_PROJECT_PATH" && dotnet ef database update --no-build --startup-project "$API_PROJECT_PATH" --connection "$CONNECTION_STRING"); then log "✅ Database updated successfully" else ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && dotnet ef database update --no-build --startup-project "$API_PROJECT_PATH" --connection "$CONNECTION_STRING") 2>&1 || true ) error "❌ Failed to update database." error " EF CLI Output: $ERROR_OUTPUT" error " This usually means the connection string in your .NET project's appsettings.$ENVIRONMENT.json is incorrect," error " or the database server is not running/accessible for environment '$ENVIRONMENT'." fi fi # Test connectivity after creation/update test_postgres_connectivity # If connectivity test fails after creation, stop the migration if [ $? -ne 0 ]; then error "❌ PostgreSQL connectivity test failed after database creation. Migration aborted for safety." error " Database may have been created but is not accessible. Please verify your connection settings." fi fi # Final verification of connection log "🔍 Verifying database connection post-creation/update..." if (cd "$DB_PROJECT_PATH" && dotnet ef migrations list --no-build --startup-project "$API_PROJECT_PATH" --connection "$CONNECTION_STRING") >/dev/null 2>&1; then log "✅ Database connectivity verification passed." else ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && dotnet ef migrations list --no-build --startup-project "$API_PROJECT_PATH" --connection "$CONNECTION_STRING") 2>&1 || true ) error "❌ Final database connectivity verification failed." error " EF CLI Output: $ERROR_OUTPUT" error " This is critical. Please review the previous error messages and your connection string for '$ENVIRONMENT'." fi # Step 2: Create database backup (only if database exists) log "📦 Step 2: Checking if database backup is needed..." # Check if the target database exists DB_EXISTS=false if PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "postgres" -c "SELECT 1 FROM pg_database WHERE datname='$DB_NAME';" 2>/dev/null | grep -q "1 row"; then DB_EXISTS=true log "✅ Target database '$DB_NAME' exists - proceeding with backup" else log "ℹ️ Target database '$DB_NAME' does not exist - skipping backup" fi if [ "$DB_EXISTS" = "true" ]; then # Define the actual backup file path (absolute) BACKUP_FILE="$BACKUP_DIR/managing_${ENVIRONMENT}_backup_${TIMESTAMP}.sql" # Backup file display path (relative to script execution) BACKUP_FILE_DISPLAY="$BACKUP_DIR_NAME/$ENVIRONMENT/managing_${ENVIRONMENT}_backup_${TIMESTAMP}.sql" # Create backup with retry logic BACKUP_SUCCESS=false for attempt in 1 2 3; do log "Backup attempt $attempt/3..." # Create real database backup using pg_dump if command -v pg_dump >/dev/null 2>&1; then if PGPASSWORD="$DB_PASSWORD" pg_dump -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" --no-password --verbose --clean --if-exists --create --format=plain > "$BACKUP_FILE" 2>/dev/null; then log "✅ Database backup created using pg_dump: $BACKUP_FILE_DISPLAY" BACKUP_SUCCESS=true break else # If pg_dump fails, fall back to EF Core migration script warn "⚠️ pg_dump failed, falling back to EF Core migration script..." # Get the first migration name to generate complete script FIRST_MIGRATION=$(get_first_migration) if [ -n "$FIRST_MIGRATION" ]; then log "📋 Generating complete backup script from initial migration: $FIRST_MIGRATION" if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --from "$FIRST_MIGRATION" --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE"); then log "✅ Complete EF Core Migration SQL Script generated: $BACKUP_FILE_DISPLAY" BACKUP_SUCCESS=true break else # Try fallback without specifying from migration ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE") 2>&1 || true) if [ $attempt -lt 3 ]; then warn "⚠️ Backup attempt $attempt failed. Retrying in 5 seconds..." warn " EF CLI Output: $ERROR_OUTPUT" sleep 5 else error "❌ Database backup failed after 3 attempts." error " EF CLI Output: $ERROR_OUTPUT" error " Migration aborted for safety reasons." fi fi else # Fallback: generate script without specifying from migration if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE"); then log "✅ EF Core Migration SQL Script generated: $BACKUP_FILE_DISPLAY" BACKUP_SUCCESS=true break else # Try fallback without specifying from migration ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE") 2>&1 || true) if [ $attempt -lt 3 ]; then warn "⚠️ Backup attempt $attempt failed. Retrying in 5 seconds..." warn " EF CLI Output: $ERROR_OUTPUT" sleep 5 else error "❌ Database backup failed after 3 attempts." error " EF CLI Output: $ERROR_OUTPUT" error " Migration aborted for safety reasons." fi fi fi fi else # If pg_dump is not available, use EF Core migration script warn "⚠️ pg_dump not available, using EF Core migration script for backup..." # Get the first migration name to generate complete script FIRST_MIGRATION=$(get_first_migration) if [ -n "$FIRST_MIGRATION" ]; then log "📋 Generating complete backup script from initial migration: $FIRST_MIGRATION" if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --from "$FIRST_MIGRATION" --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE"); then log "✅ Complete EF Core Migration SQL Script generated: $BACKUP_FILE_DISPLAY" BACKUP_SUCCESS=true break else # Try fallback without specifying from migration ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE") 2>&1 || true) if [ $attempt -lt 3 ]; then warn "⚠️ Backup attempt $attempt failed. Retrying in 5 seconds..." warn " EF CLI Output: $ERROR_OUTPUT" sleep 5 else error "❌ Database backup failed after 3 attempts." error " EF CLI Output: $ERROR_OUTPUT" error " Migration aborted for safety reasons." fi fi else # Fallback: generate script without specifying from migration if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE"); then log "✅ EF Core Migration SQL Script generated: $BACKUP_FILE_DISPLAY" BACKUP_SUCCESS=true break else # Try fallback without specifying from migration ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$BACKUP_FILE") 2>&1 || true) if [ $attempt -lt 3 ]; then warn "⚠️ Backup attempt $attempt failed. Retrying in 5 seconds..." warn " EF CLI Output: $ERROR_OUTPUT" sleep 5 else error "❌ Database backup failed after 3 attempts." error " EF CLI Output: $ERROR_OUTPUT" error " Migration aborted for safety reasons." fi fi fi fi done # Check if backup was successful before proceeding if [ "$BACKUP_SUCCESS" != "true" ]; then error "❌ Database backup failed. Migration aborted for safety." error " Cannot proceed with migration without a valid backup." error " Please resolve backup issues and try again." fi fi # Step 2.5: Check for pending model changes and create migrations if needed log "🔍 Step 2.5: Checking for pending model changes..." # Check if there are any pending model changes that need migrations PENDING_CHANGES_OUTPUT=$( (cd "$DB_PROJECT_PATH" && dotnet ef migrations add --dry-run --startup-project "$API_PROJECT_PATH" --name "PendingChanges_${TIMESTAMP}") 2>&1 || true ) if echo "$PENDING_CHANGES_OUTPUT" | grep -q "No pending model changes"; then log "✅ No pending model changes detected - existing migrations are up to date" else log "⚠️ Pending model changes detected that require new migrations" echo "" echo "==========================================" echo "📋 PENDING MODEL CHANGES DETECTED" echo "==========================================" echo "The following changes require new migrations:" echo "$PENDING_CHANGES_OUTPUT" echo "" echo "Would you like to create a new migration now?" echo "==========================================" echo "" read -p "🔧 Create new migration? (y/n): " create_migration if [[ "$create_migration" =~ ^[Yy]$ ]]; then log "📝 Creating new migration..." # Get migration name from user read -p "📝 Enter migration name (or press Enter for auto-generated name): " migration_name if [ -z "$migration_name" ]; then migration_name="Migration_${TIMESTAMP}" fi # Create the migration if (cd "$DB_PROJECT_PATH" && dotnet ef migrations add "$migration_name" --startup-project "$API_PROJECT_PATH"); then log "✅ Migration '$migration_name' created successfully" # Show the created migration file LATEST_MIGRATION=$(find "$DB_PROJECT_PATH/Migrations" -name "*${migration_name}.cs" | head -1) if [ -n "$LATEST_MIGRATION" ]; then log "📄 Migration file created: $(basename "$LATEST_MIGRATION")" log " Location: $LATEST_MIGRATION" fi else ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && dotnet ef migrations add "$migration_name" --startup-project "$API_PROJECT_PATH") 2>&1 || true ) error "❌ Failed to create migration '$migration_name'" error " EF CLI Output: $ERROR_OUTPUT" error " Please resolve the model issues and try again." fi else log "⚠️ Skipping migration creation. Proceeding with existing migrations only." log " Note: If there are pending changes, the migration may fail." fi fi # Step 3: Run Migration (This effectively is a retry if previous "update" failed, or a final apply) log "🔄 Step 3: Running database migration (final application of pending migrations)..." # Check if database exists and create it if needed before applying migrations log "🔍 Step 3a: Ensuring target database exists..." if [ "$TARGET_DB_EXISTS" = "false" ]; then log "🔧 Creating database '$DB_NAME'..." if command -v psql >/dev/null 2>&1; then if PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d postgres -c "CREATE DATABASE \"$DB_NAME\";" >/dev/null 2>&1; then log "✅ Database '$DB_NAME' created successfully" else error "❌ Failed to create database '$DB_NAME'" error " Please verify you have sufficient privileges to create databases." fi else error "❌ psql not available, cannot create database. Please create database '$DB_NAME' manually." fi fi # Generate migration script first (Microsoft recommended approach) MIGRATION_SCRIPT="$BACKUP_DIR/migration_${ENVIRONMENT}_${TIMESTAMP}.sql" log "📝 Step 3b: Generating migration script for pending migrations..." # Check if database is empty (no tables) to determine the best approach log "🔍 Checking if database has existing tables..." DB_HAS_TABLES=false if command -v psql >/dev/null 2>&1; then TABLE_COUNT=$(PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -t -c "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public';" 2>/dev/null | tr -d ' ' || echo "0") if [ "$TABLE_COUNT" -gt 0 ]; then DB_HAS_TABLES=true log "✅ Database has $TABLE_COUNT existing tables - using idempotent script generation" else log "⚠️ Database appears to be empty - using full migration script generation" fi else log "⚠️ psql not available - assuming database has tables and using idempotent script generation" DB_HAS_TABLES=true fi # Generate migration script based on database state if [ "$DB_HAS_TABLES" = "true" ]; then # For databases with existing tables, we need to generate a complete script # that includes all migrations from the beginning log "📝 Generating complete migration script from initial migration..." # Get the first migration name to generate script from the beginning FIRST_MIGRATION=$(get_first_migration) if [ -n "$FIRST_MIGRATION" ]; then log "📋 Generating complete script for all migrations (idempotent)..." if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT"); then log "✅ Complete migration script generated (all migrations, idempotent): $(basename "$MIGRATION_SCRIPT")" else ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT") 2>&1 || true ) error "❌ Failed to generate complete migration script." error " EF CLI Output: $ERROR_OUTPUT" error " Check the .NET project logs for detailed errors." error " Backup script available at: $BACKUP_FILE_DISPLAY" fi else # Fallback: generate script without specifying from migration log "📝 Fallback: Generating migration script without specifying from migration..." if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT"); then log "✅ Migration script generated (idempotent): $(basename "$MIGRATION_SCRIPT")" else ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --idempotent --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT") 2>&1 || true ) error "❌ Failed to generate idempotent migration script." error " EF CLI Output: $ERROR_OUTPUT" error " Check the .NET project logs for detailed errors." error " Backup script available at: $BACKUP_FILE_DISPLAY" fi fi else # Use full script generation for empty databases (generate script from the very beginning) log "📝 Generating full migration script for empty database..." # Get the first migration name to generate script from the beginning FIRST_MIGRATION=$(get_first_migration) if [ -n "$FIRST_MIGRATION" ]; then log "📋 Generating complete script for all migrations..." if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT"); then log "✅ Complete migration script generated (all migrations): $(basename "$MIGRATION_SCRIPT")" else ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT") 2>&1 || true ) error "❌ Failed to generate complete migration script." error " EF CLI Output: $ERROR_OUTPUT" error " Check the .NET project logs for detailed errors." error " Backup script available at: $BACKUP_FILE_DISPLAY" fi else # Fallback: generate script without specifying from migration log "📝 Fallback: Generating migration script without specifying from migration..." if (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT"); then log "✅ Migration script generated (fallback): $(basename "$MIGRATION_SCRIPT")" else ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && ASPNETCORE_ENVIRONMENT="$ENVIRONMENT" dotnet ef migrations script --no-build --startup-project "$API_PROJECT_PATH" --output "$MIGRATION_SCRIPT") 2>&1 || true ) error "❌ Failed to generate fallback migration script." error " EF CLI Output: $ERROR_OUTPUT" error " Check the .NET project logs for detailed errors." error " Backup script available at: $BACKUP_FILE_DISPLAY" fi fi fi # Show the migration script path to the user for review echo "" echo "==========================================" echo "📋 MIGRATION SCRIPT READY FOR REVIEW" echo "==========================================" echo "Generated script: $MIGRATION_SCRIPT" echo "Environment: $ENVIRONMENT" echo "Database: $DB_HOST:$DB_PORT/$DB_NAME" echo "" # Show a preview of the migration script content if [ -f "$MIGRATION_SCRIPT" ]; then SCRIPT_SIZE=$(wc -l < "$MIGRATION_SCRIPT") echo "📄 Migration script contains $SCRIPT_SIZE lines" # Show first 20 lines as preview echo "" echo "📋 PREVIEW (first 20 lines):" echo "----------------------------------------" head -20 "$MIGRATION_SCRIPT" | sed 's/^/ /' if [ "$SCRIPT_SIZE" -gt 20 ]; then echo " ... (showing first 20 lines of $SCRIPT_SIZE total)" fi echo "----------------------------------------" echo "" fi echo "⚠️ IMPORTANT: Please review the migration script before proceeding!" echo " You can examine the full script with: cat $MIGRATION_SCRIPT" echo " Or open it in your editor to review the changes." echo "" echo "==========================================" echo "" # Ask for user confirmation read -p "🔍 Have you reviewed the migration script and want to proceed? Type 'yes' to continue: " user_confirmation if [ "$user_confirmation" != "yes" ]; then log "❌ Migration cancelled by user." log " Migration script is available at: $(basename "$MIGRATION_SCRIPT")" log " You can apply it manually later with:" log " PGPASSWORD=\"$DB_PASSWORD\" psql -h \"$DB_HOST\" -p \"$DB_PORT\" -U \"$DB_USER\" -d \"$DB_NAME\" -f \"$MIGRATION_SCRIPT\"" exit 0 fi log "✅ User confirmed migration. Proceeding with database update..." # Apply the migration script using psql (recommended approach) log "🔧 Step 3c: Applying migration script to database..." if command -v psql >/dev/null 2>&1; then if PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -f "$MIGRATION_SCRIPT" >/dev/null 2>&1; then log "✅ Migration script applied successfully to database" else ERROR_OUTPUT=$( (PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -f "$MIGRATION_SCRIPT") 2>&1 || true ) error "❌ Failed to apply migration script to database" error " PSQL Output: $ERROR_OUTPUT" error " Migration script available at: $(basename "$MIGRATION_SCRIPT")" fi else # Fallback to EF Core database update if psql is not available log "🔄 psql not available, falling back to EF Core database update..." if (cd "$DB_PROJECT_PATH" && dotnet ef database update --no-build --startup-project "$API_PROJECT_PATH" --connection "$CONNECTION_STRING"); then log "✅ Database migration completed successfully using EF Core." else ERROR_OUTPUT=$( (cd "$DB_PROJECT_PATH" && dotnet ef database update --no-build --startup-project "$API_PROJECT_PATH" --connection "$CONNECTION_STRING") 2>&1 || true ) error "❌ Database migration failed during final update." error " EF CLI Output: $ERROR_OUTPUT" error " Check the .NET project logs for detailed errors." error " Backup script available at: $BACKUP_FILE_DISPLAY" fi fi # Save a copy of the migration script for reference before cleaning up MIGRATION_SCRIPT_COPY="$BACKUP_DIR/migration_${ENVIRONMENT}_${TIMESTAMP}_applied.sql" if [ -f "$MIGRATION_SCRIPT" ]; then cp "$MIGRATION_SCRIPT" "$MIGRATION_SCRIPT_COPY" log "📝 Migration script saved for reference: $(basename "$MIGRATION_SCRIPT_COPY")" fi # Clean up temporary migration script after successful application rm -f "$MIGRATION_SCRIPT" # Step 4: Verify Migration log "🔍 Step 4: Verifying migration status..." # List migrations to check applied status MIGRATION_LIST_OUTPUT=$( (cd "$DB_PROJECT_PATH" && dotnet ef migrations list --no-build --startup-project "$API_PROJECT_PATH" --connection "$CONNECTION_STRING") 2>&1 ) log "📋 Current migration status:\n$MIGRATION_LIST_OUTPUT" # Check if there are any pending migrations after update PENDING_MIGRATIONS=$(echo "$MIGRATION_LIST_OUTPUT" | grep -c "\[ \]" || echo "0") PENDING_MIGRATIONS=$(echo "$PENDING_MIGRATIONS" | tr -d '\n') # Remove any newlines if [ "$PENDING_MIGRATIONS" -gt 0 ]; then warn "⚠️ WARNING: $PENDING_MIGRATIONS pending migration(s) found after update." warn " This indicates the 'dotnet ef database update' command may not have fully completed." else log "✅ All migrations appear to be applied successfully." fi # --- Step 5: Cleanup Backups (keep only 5 dumps max) --- log "🧹 Step 5: Cleaning up old backups..." # Keep only the last 5 backups for this environment (in the environment-specific subfolder) ls -t "$BACKUP_DIR"/managing_${ENVIRONMENT}_backup_*.sql 2>/dev/null | tail -n +6 | xargs -r rm -f || true # Added -f for force removal log "✅ Kept last 5 backups for $ENVIRONMENT environment in $BACKUP_DIR_NAME/$ENVIRONMENT/" # Success Summary log "🎉 Migration completed successfully for environment: $ENVIRONMENT!" log "📁 EF Core Migration SQL Script: $BACKUP_FILE_DISPLAY" log "📝 Full Log file: $LOG_FILE" echo "" echo "==========================================" echo "📋 MIGRATION SUMMARY" echo "==========================================" echo "Environment: $ENVIRONMENT" echo "Timestamp: $TIMESTAMP" echo "Status: ✅ SUCCESS" echo "EF Core SQL Backup: $BACKUP_FILE_DISPLAY" echo "Log: $LOG_FILE" echo "=========================================="