mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-10 07:38:04 -05:00
fix(backend): Improve migration script with nuke step and table exclusions
- Add Step 0 to nuke destination database with confirmation (type 'NUKE') - Exclude large execution tables to speed up migration: - AgentGraphExecution (1.3 GB) - AgentNodeExecution (6 GB) - AgentNodeExecutionInputOutput (30 GB) - AgentNodeExecutionKeyValueData - NotificationEvent (94 MB) - Fix set -e issue with parse_args validation - Clean up script structure and documentation Reduces migration from ~37 GB to ~544 MB for initial cutover. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
634
autogpt_platform/backend/scripts/migrate_to_gcp.sh
Normal file → Executable file
634
autogpt_platform/backend/scripts/migrate_to_gcp.sh
Normal file → Executable file
@@ -2,31 +2,25 @@
|
||||
#
|
||||
# Database Migration Script: Supabase to GCP Cloud SQL
|
||||
#
|
||||
# This script automates the migration of the AutoGPT Platform database
|
||||
# from a source PostgreSQL database to a destination PostgreSQL database.
|
||||
# This script migrates the AutoGPT Platform database from Supabase to a new PostgreSQL instance.
|
||||
#
|
||||
# Migration Steps:
|
||||
# 0. Nuke destination database (drop schema, recreate, apply migrations)
|
||||
# 1. Export platform schema data from source
|
||||
# 2. Export auth.users data from source (for password hashes, OAuth IDs)
|
||||
# 3. Import platform schema data to destination
|
||||
# 4. Update User table in destination with auth data
|
||||
# 5. Refresh materialized views
|
||||
#
|
||||
# Prerequisites:
|
||||
# - pg_dump and psql (PostgreSQL 15+)
|
||||
# - poetry installed for Python scripts
|
||||
# - poetry installed (for Prisma migrations)
|
||||
# - Source and destination databases accessible
|
||||
#
|
||||
# Usage:
|
||||
# cd backend
|
||||
# chmod +x scripts/migrate_to_gcp.sh
|
||||
# ./scripts/migrate_to_gcp.sh \
|
||||
# --source "postgresql://user:pass@host:5432/db?schema=platform" \
|
||||
# --dest "postgresql://user:pass@host:5433/db?schema=platform"
|
||||
#
|
||||
# Examples:
|
||||
# # Full migration
|
||||
# ./scripts/migrate_to_gcp.sh \
|
||||
# --source "postgresql://postgres.xxx:password@aws-0-us-east-1.pooler.supabase.com:5432/postgres?schema=platform" \
|
||||
# --dest "postgresql://postgres:password@127.0.0.1:5433/postgres?schema=platform"
|
||||
#
|
||||
# # Dry run (preview only)
|
||||
# ./scripts/migrate_to_gcp.sh \
|
||||
# --source "postgresql://..." \
|
||||
# --dest "postgresql://..." \
|
||||
# --dry-run
|
||||
# --source 'postgresql://user:pass@host:5432/db?schema=platform' \
|
||||
# --dest 'postgresql://user:pass@host:5432/db?schema=platform'
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
@@ -36,13 +30,10 @@ RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
NC='\033[0m'
|
||||
|
||||
# Script directory
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
BACKEND_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
# Configuration
|
||||
BACKUP_DIR="${BACKEND_DIR}/migration_backups"
|
||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||
|
||||
@@ -50,308 +41,350 @@ TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||
SOURCE_URL=""
|
||||
DEST_URL=""
|
||||
DRY_RUN=false
|
||||
SKIP_AUTH_MIGRATION=false
|
||||
|
||||
# Logging
|
||||
log_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
||||
log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
|
||||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# Usage help
|
||||
usage() {
|
||||
cat << EOF
|
||||
Usage: $(basename "$0") --source <url> --dest <url> [options]
|
||||
|
||||
Required:
|
||||
--source <url> Source database URL (e.g., postgresql://user:pass@host:5432/db?schema=platform)
|
||||
--dest <url> Destination database URL (e.g., postgresql://user:pass@host:5433/db?schema=platform)
|
||||
--source <url> Source database URL with ?schema=platform
|
||||
--dest <url> Destination database URL with ?schema=platform
|
||||
|
||||
Options:
|
||||
--dry-run Preview migration without making changes
|
||||
--skip-auth Skip the Supabase auth migration step (if already done)
|
||||
--help Show this help message
|
||||
--dry-run Preview without making changes
|
||||
--help Show this help
|
||||
|
||||
Examples:
|
||||
# Migrate from Supabase to GCP
|
||||
$(basename "$0") \\
|
||||
--source "postgresql://postgres.xxx:pass@supabase.com:5432/postgres?schema=platform" \\
|
||||
--dest "postgresql://postgres:pass@127.0.0.1:5433/postgres?schema=platform"
|
||||
|
||||
# Dry run to preview
|
||||
$(basename "$0") --source "..." --dest "..." --dry-run
|
||||
Migration Steps:
|
||||
0. Nuke destination database (DROP SCHEMA, recreate, apply Prisma migrations)
|
||||
1. Export platform schema data from source (READ-ONLY)
|
||||
2. Export auth.users data from source (READ-ONLY)
|
||||
3. Import platform data to destination
|
||||
4. Update User table with auth data (passwords, OAuth IDs)
|
||||
5. Refresh materialized views
|
||||
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Parse command line arguments
|
||||
parse_args() {
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--source)
|
||||
SOURCE_URL="$2"
|
||||
shift 2
|
||||
;;
|
||||
--dest)
|
||||
DEST_URL="$2"
|
||||
shift 2
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=true
|
||||
shift
|
||||
;;
|
||||
--skip-auth)
|
||||
SKIP_AUTH_MIGRATION=true
|
||||
shift
|
||||
;;
|
||||
--help|-h)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown option: $1"
|
||||
usage
|
||||
;;
|
||||
--source) SOURCE_URL="$2"; shift 2 ;;
|
||||
--dest) DEST_URL="$2"; shift 2 ;;
|
||||
--dry-run) DRY_RUN=true; shift ;;
|
||||
--help|-h) usage ;;
|
||||
*) log_error "Unknown option: $1"; usage ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Validate required arguments
|
||||
if [[ -z "$SOURCE_URL" ]]; then
|
||||
log_error "Missing required argument: --source"
|
||||
log_error "Missing --source"
|
||||
usage
|
||||
fi
|
||||
|
||||
if [[ -z "$DEST_URL" ]]; then
|
||||
log_error "Missing required argument: --dest"
|
||||
log_error "Missing --dest"
|
||||
usage
|
||||
fi
|
||||
}
|
||||
|
||||
# Extract schema from URL (default to 'platform' if not specified)
|
||||
get_schema_from_url() {
|
||||
local url="$1"
|
||||
local schema=$(echo "$url" | grep -oP 'schema=\K[^&]+' || echo "platform")
|
||||
echo "$schema"
|
||||
local schema=$(echo "$url" | sed -n 's/.*schema=\([^&]*\).*/\1/p')
|
||||
echo "${schema:-platform}"
|
||||
}
|
||||
|
||||
# Get URL without query params (for pg_dump/pg_restore)
|
||||
get_base_url() {
|
||||
local url="$1"
|
||||
echo "${url%%\?*}"
|
||||
}
|
||||
|
||||
# Check prerequisites
|
||||
check_prerequisites() {
|
||||
log_info "Checking prerequisites..."
|
||||
|
||||
local missing=()
|
||||
|
||||
if ! command -v pg_dump &> /dev/null; then
|
||||
missing+=("pg_dump")
|
||||
fi
|
||||
|
||||
if ! command -v psql &> /dev/null; then
|
||||
missing+=("psql")
|
||||
fi
|
||||
|
||||
if ! command -v poetry &> /dev/null; then
|
||||
missing+=("poetry")
|
||||
fi
|
||||
|
||||
if [ ${#missing[@]} -ne 0 ]; then
|
||||
log_error "Missing required tools: ${missing[*]}"
|
||||
log_error "Please install them before continuing."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check PostgreSQL version
|
||||
local pg_version=$(pg_dump --version | grep -oE '[0-9]+' | head -1)
|
||||
if [ "$pg_version" -lt 15 ]; then
|
||||
log_warn "PostgreSQL version $pg_version detected. Version 15+ recommended."
|
||||
fi
|
||||
|
||||
log_success "All prerequisites satisfied"
|
||||
}
|
||||
|
||||
# Test database connections
|
||||
test_connections() {
|
||||
log_info "Testing source database connection..."
|
||||
if ! psql "${SOURCE_URL}" -c "SELECT 1" &> /dev/null; then
|
||||
log_error "Cannot connect to source database"
|
||||
log_error "URL: ${SOURCE_URL%%:*}:****@${SOURCE_URL#*@}"
|
||||
exit 1
|
||||
fi
|
||||
log_success "Source database connection OK"
|
||||
local source_base=$(get_base_url "$SOURCE_URL")
|
||||
local dest_base=$(get_base_url "$DEST_URL")
|
||||
|
||||
log_info "Testing destination database connection..."
|
||||
if ! psql "${DEST_URL}" -c "SELECT 1" &> /dev/null; then
|
||||
log_error "Cannot connect to destination database"
|
||||
log_error "URL: ${DEST_URL%%:*}:****@${DEST_URL#*@}"
|
||||
log_info "Testing source connection..."
|
||||
if ! psql "${source_base}" -c "SELECT 1" > /dev/null 2>&1; then
|
||||
log_error "Cannot connect to source database"
|
||||
psql "${source_base}" -c "SELECT 1" 2>&1 || true
|
||||
exit 1
|
||||
fi
|
||||
log_success "Destination database connection OK"
|
||||
log_success "Source connection OK"
|
||||
|
||||
log_info "Testing destination connection..."
|
||||
if ! psql "${dest_base}" -c "SELECT 1" > /dev/null 2>&1; then
|
||||
log_error "Cannot connect to destination database"
|
||||
psql "${dest_base}" -c "SELECT 1" 2>&1 || true
|
||||
exit 1
|
||||
fi
|
||||
log_success "Destination connection OK"
|
||||
}
|
||||
|
||||
# Run auth migration on source database
|
||||
run_auth_migration() {
|
||||
if [[ "$SKIP_AUTH_MIGRATION" == true ]]; then
|
||||
log_info "Skipping auth migration (--skip-auth flag set)"
|
||||
return
|
||||
fi
|
||||
# ============================================
|
||||
# STEP 0: Nuke destination database
|
||||
# ============================================
|
||||
nuke_destination() {
|
||||
local schema=$(get_schema_from_url "$DEST_URL")
|
||||
local dest_base=$(get_base_url "$DEST_URL")
|
||||
|
||||
log_info "Running auth migration on source database..."
|
||||
|
||||
cd "${BACKEND_DIR}"
|
||||
|
||||
# Check if auth.users table exists
|
||||
local auth_exists=$(psql "${SOURCE_URL}" -t -c "SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_schema = 'auth' AND table_name = 'users')" 2>/dev/null | tr -d ' ')
|
||||
|
||||
if [[ "$auth_exists" != "t" ]]; then
|
||||
log_warn "No auth.users table found in source database"
|
||||
log_warn "Skipping Supabase auth migration (may not be a Supabase database)"
|
||||
return
|
||||
fi
|
||||
log_info "=== STEP 0: Nuking destination database ==="
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
log_info "DRY RUN: Would run auth migration script"
|
||||
DATABASE_URL="${SOURCE_URL}" \
|
||||
DIRECT_URL="${SOURCE_URL}" \
|
||||
poetry run python scripts/migrate_supabase_users.py --dry-run
|
||||
else
|
||||
# Apply Prisma migrations first
|
||||
log_info "Applying Prisma migrations to source..."
|
||||
DATABASE_URL="${SOURCE_URL}" \
|
||||
DIRECT_URL="${SOURCE_URL}" \
|
||||
poetry run prisma migrate deploy
|
||||
|
||||
# Run the Supabase user migration script
|
||||
log_info "Migrating auth data from Supabase auth.users..."
|
||||
DATABASE_URL="${SOURCE_URL}" \
|
||||
DIRECT_URL="${SOURCE_URL}" \
|
||||
poetry run python scripts/migrate_supabase_users.py
|
||||
log_info "DRY RUN: Would drop and recreate schema '${schema}' in destination"
|
||||
return
|
||||
fi
|
||||
|
||||
log_success "Auth migration completed"
|
||||
# Show what exists in destination
|
||||
log_info "Current destination state:"
|
||||
local user_count=$(psql "${dest_base}" -t -c "SELECT COUNT(*) FROM ${schema}.\"User\"" 2>/dev/null | tr -d ' ' || echo "0")
|
||||
local graph_count=$(psql "${dest_base}" -t -c "SELECT COUNT(*) FROM ${schema}.\"AgentGraph\"" 2>/dev/null | tr -d ' ' || echo "0")
|
||||
echo " - Users: ${user_count}"
|
||||
echo " - AgentGraphs: ${graph_count}"
|
||||
|
||||
echo ""
|
||||
log_warn "⚠️ WARNING: This will PERMANENTLY DELETE all data in the destination database!"
|
||||
log_warn "Schema '${schema}' will be dropped and recreated."
|
||||
echo ""
|
||||
read -p "Type 'NUKE' to confirm deletion: " -r
|
||||
echo ""
|
||||
|
||||
if [[ "$REPLY" != "NUKE" ]]; then
|
||||
log_info "Cancelled - destination not modified"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
log_info "Dropping schema '${schema}'..."
|
||||
psql "${dest_base}" -c "DROP SCHEMA IF EXISTS ${schema} CASCADE;"
|
||||
|
||||
log_info "Recreating schema '${schema}'..."
|
||||
psql "${dest_base}" -c "CREATE SCHEMA ${schema};"
|
||||
|
||||
log_info "Applying Prisma migrations..."
|
||||
cd "${BACKEND_DIR}"
|
||||
DATABASE_URL="${DEST_URL}" DIRECT_URL="${DEST_URL}" poetry run prisma migrate deploy
|
||||
|
||||
log_success "Destination database reset complete"
|
||||
}
|
||||
|
||||
# Export database from source
|
||||
export_database() {
|
||||
# ============================================
|
||||
# STEP 1: Export platform schema data
|
||||
# ============================================
|
||||
export_platform_data() {
|
||||
local schema=$(get_schema_from_url "$SOURCE_URL")
|
||||
local base_url=$(get_base_url "$SOURCE_URL")
|
||||
local backup_file="${BACKUP_DIR}/platform_backup_${TIMESTAMP}.dump"
|
||||
|
||||
log_info "Exporting database from source (schema: ${schema})..."
|
||||
local output_file="${BACKUP_DIR}/platform_data_${TIMESTAMP}.sql"
|
||||
|
||||
log_info "=== STEP 1: Exporting platform schema data ==="
|
||||
mkdir -p "${BACKUP_DIR}"
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
log_info "DRY RUN: Would export schema '${schema}' to ${backup_file}"
|
||||
# Create empty file for dry run
|
||||
touch "${backup_file}"
|
||||
echo "$backup_file"
|
||||
log_info "DRY RUN: Would export schema '${schema}' to ${output_file}"
|
||||
log_info "DRY RUN: Excluding large execution tables"
|
||||
touch "$output_file"
|
||||
echo "$output_file"
|
||||
return
|
||||
fi
|
||||
|
||||
log_info "This may take a while depending on database size..."
|
||||
log_info "Exporting from schema: ${schema}"
|
||||
log_info "EXCLUDING: AgentGraphExecution, AgentNodeExecution, AgentNodeExecutionInputOutput, AgentNodeExecutionKeyValueData, NotificationEvent"
|
||||
|
||||
pg_dump "${base_url}" \
|
||||
--schema="${schema}" \
|
||||
--format=custom \
|
||||
--format=plain \
|
||||
--no-owner \
|
||||
--no-privileges \
|
||||
--verbose \
|
||||
--file="${backup_file}" 2>&1 | while read line; do
|
||||
echo -e "${BLUE}[pg_dump]${NC} $line"
|
||||
done
|
||||
|
||||
local file_size=$(du -h "${backup_file}" | cut -f1)
|
||||
log_success "Database exported to ${backup_file} (${file_size})"
|
||||
|
||||
echo "$backup_file"
|
||||
}
|
||||
|
||||
# Set up schema on destination
|
||||
setup_dest_schema() {
|
||||
log_info "Setting up schema on destination database..."
|
||||
|
||||
cd "${BACKEND_DIR}"
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
log_info "DRY RUN: Would deploy Prisma schema to destination"
|
||||
return
|
||||
fi
|
||||
|
||||
# Deploy Prisma schema to destination
|
||||
DATABASE_URL="${DEST_URL}" \
|
||||
DIRECT_URL="${DEST_URL}" \
|
||||
poetry run prisma migrate deploy
|
||||
|
||||
log_success "Destination schema created via Prisma migrations"
|
||||
}
|
||||
|
||||
# Import data to destination
|
||||
import_database() {
|
||||
local backup_file="$1"
|
||||
local schema=$(get_schema_from_url "$DEST_URL")
|
||||
local base_url=$(get_base_url "$DEST_URL")
|
||||
|
||||
log_info "Importing data to destination database..."
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
log_info "DRY RUN: Would import ${backup_file} to destination"
|
||||
return
|
||||
fi
|
||||
|
||||
log_info "This may take a while..."
|
||||
|
||||
pg_restore \
|
||||
--dbname="${base_url}" \
|
||||
--schema="${schema}" \
|
||||
--no-owner \
|
||||
--no-privileges \
|
||||
--verbose \
|
||||
--data-only \
|
||||
--disable-triggers \
|
||||
"${backup_file}" 2>&1 | while read line; do
|
||||
echo -e "${BLUE}[pg_restore]${NC} $line"
|
||||
done || true # pg_restore returns non-zero on warnings
|
||||
--exclude-table="${schema}.AgentGraphExecution" \
|
||||
--exclude-table="${schema}.AgentNodeExecution" \
|
||||
--exclude-table="${schema}.AgentNodeExecutionInputOutput" \
|
||||
--exclude-table="${schema}.AgentNodeExecutionKeyValueData" \
|
||||
--exclude-table="${schema}.NotificationEvent" \
|
||||
--file="${output_file}" 2>&1
|
||||
|
||||
log_success "Data import completed"
|
||||
# Remove Supabase-specific commands that break import
|
||||
sed -i.bak '/\\restrict/d' "${output_file}"
|
||||
rm -f "${output_file}.bak"
|
||||
|
||||
local size=$(du -h "${output_file}" | cut -f1)
|
||||
log_success "Platform data exported: ${output_file} (${size})"
|
||||
echo "$output_file"
|
||||
}
|
||||
|
||||
# Refresh materialized views
|
||||
# ============================================
|
||||
# STEP 2: Export auth.users data
|
||||
# ============================================
|
||||
export_auth_data() {
|
||||
local base_url=$(get_base_url "$SOURCE_URL")
|
||||
local output_file="${BACKUP_DIR}/auth_users_${TIMESTAMP}.csv"
|
||||
|
||||
log_info "=== STEP 2: Exporting auth.users data ==="
|
||||
|
||||
# Check if auth.users exists
|
||||
local auth_exists=$(psql "${base_url}" -t -c "SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_schema = 'auth' AND table_name = 'users')" 2>/dev/null | tr -d ' ')
|
||||
|
||||
if [[ "$auth_exists" != "t" ]]; then
|
||||
log_warn "No auth.users table found - skipping auth export"
|
||||
echo ""
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
log_info "DRY RUN: Would export auth.users to ${output_file}"
|
||||
touch "$output_file"
|
||||
echo "$output_file"
|
||||
return
|
||||
fi
|
||||
|
||||
log_info "Extracting auth data (passwords, OAuth IDs, email verification)..."
|
||||
|
||||
psql "${base_url}" -c "\COPY (
|
||||
SELECT
|
||||
id,
|
||||
encrypted_password,
|
||||
(email_confirmed_at IS NOT NULL) as email_verified,
|
||||
CASE
|
||||
WHEN raw_app_meta_data->>'provider' = 'google'
|
||||
THEN raw_app_meta_data->>'provider_id'
|
||||
ELSE NULL
|
||||
END as google_id
|
||||
FROM auth.users
|
||||
WHERE encrypted_password IS NOT NULL
|
||||
OR raw_app_meta_data->>'provider' = 'google'
|
||||
) TO '${output_file}' WITH CSV HEADER"
|
||||
|
||||
local count=$(wc -l < "${output_file}" | tr -d ' ')
|
||||
log_success "Auth data exported: ${output_file} (${count} rows including header)"
|
||||
echo "$output_file"
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# STEP 3: Import platform data to destination
|
||||
# ============================================
|
||||
import_platform_data() {
|
||||
local platform_file="$1"
|
||||
local dest_base=$(get_base_url "$DEST_URL")
|
||||
|
||||
log_info "=== STEP 3: Importing platform data to destination ==="
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
log_info "DRY RUN: Would import ${platform_file} to destination"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ ! -f "$platform_file" ]]; then
|
||||
log_error "Platform data file not found: ${platform_file}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_info "Importing platform data (this may take a while)..."
|
||||
|
||||
# Import with error logging
|
||||
psql "${dest_base}" -f "${platform_file}" 2>&1 | tee "${BACKUP_DIR}/import_log_${TIMESTAMP}.txt" | head -100
|
||||
|
||||
log_success "Platform data import completed"
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# STEP 4: Update User table with auth data
|
||||
# ============================================
|
||||
update_user_auth_data() {
|
||||
local auth_file="$1"
|
||||
local schema=$(get_schema_from_url "$DEST_URL")
|
||||
local dest_base=$(get_base_url "$DEST_URL")
|
||||
|
||||
log_info "=== STEP 4: Updating User table with auth data ==="
|
||||
|
||||
if [[ -z "$auth_file" || ! -f "$auth_file" ]]; then
|
||||
log_warn "No auth data file - skipping User auth update"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
log_info "DRY RUN: Would update User table with auth data"
|
||||
return
|
||||
fi
|
||||
|
||||
log_info "Creating temporary table for auth data..."
|
||||
|
||||
psql "${dest_base}" << EOF
|
||||
-- Create temp table for auth data
|
||||
CREATE TEMP TABLE temp_auth_users (
|
||||
id UUID,
|
||||
encrypted_password TEXT,
|
||||
email_verified BOOLEAN,
|
||||
google_id TEXT
|
||||
);
|
||||
|
||||
-- Import CSV
|
||||
\COPY temp_auth_users FROM '${auth_file}' WITH CSV HEADER;
|
||||
|
||||
-- Update User table with password hashes
|
||||
UPDATE ${schema}."User" u
|
||||
SET "passwordHash" = t.encrypted_password
|
||||
FROM temp_auth_users t
|
||||
WHERE u.id = t.id
|
||||
AND t.encrypted_password IS NOT NULL
|
||||
AND u."passwordHash" IS NULL;
|
||||
|
||||
-- Update User table with email verification
|
||||
UPDATE ${schema}."User" u
|
||||
SET "emailVerified" = t.email_verified
|
||||
FROM temp_auth_users t
|
||||
WHERE u.id = t.id
|
||||
AND t.email_verified = true;
|
||||
|
||||
-- Update User table with Google OAuth IDs
|
||||
UPDATE ${schema}."User" u
|
||||
SET "googleId" = t.google_id
|
||||
FROM temp_auth_users t
|
||||
WHERE u.id = t.id
|
||||
AND t.google_id IS NOT NULL
|
||||
AND u."googleId" IS NULL;
|
||||
|
||||
-- Show results
|
||||
SELECT
|
||||
'Total Users' as metric, COUNT(*)::text as value FROM ${schema}."User"
|
||||
UNION ALL
|
||||
SELECT 'With Password', COUNT(*)::text FROM ${schema}."User" WHERE "passwordHash" IS NOT NULL
|
||||
UNION ALL
|
||||
SELECT 'With Google OAuth', COUNT(*)::text FROM ${schema}."User" WHERE "googleId" IS NOT NULL
|
||||
UNION ALL
|
||||
SELECT 'Email Verified', COUNT(*)::text FROM ${schema}."User" WHERE "emailVerified" = true;
|
||||
|
||||
DROP TABLE temp_auth_users;
|
||||
EOF
|
||||
|
||||
log_success "User auth data updated"
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# STEP 5: Refresh materialized views
|
||||
# ============================================
|
||||
refresh_views() {
|
||||
local schema=$(get_schema_from_url "$DEST_URL")
|
||||
local dest_base=$(get_base_url "$DEST_URL")
|
||||
|
||||
log_info "Refreshing materialized views..."
|
||||
log_info "=== STEP 5: Refreshing materialized views ==="
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
log_info "DRY RUN: Would refresh materialized views"
|
||||
return
|
||||
fi
|
||||
|
||||
psql "${DEST_URL}" <<EOF
|
||||
psql "${dest_base}" << EOF
|
||||
SET search_path TO ${schema};
|
||||
|
||||
-- Refresh materialized views
|
||||
REFRESH MATERIALIZED VIEW "mv_agent_run_counts";
|
||||
REFRESH MATERIALIZED VIEW "mv_review_stats";
|
||||
|
||||
-- Reset sequences
|
||||
SELECT setval(
|
||||
pg_get_serial_sequence('${schema}."SearchTerms"', 'id'),
|
||||
COALESCE((SELECT MAX(id) FROM "${schema}"."SearchTerms"), 0) + 1,
|
||||
COALESCE((SELECT MAX(id) FROM ${schema}."SearchTerms"), 0) + 1,
|
||||
false
|
||||
);
|
||||
EOF
|
||||
@@ -359,116 +392,91 @@ EOF
|
||||
log_success "Materialized views refreshed"
|
||||
}
|
||||
|
||||
# Verify migration
|
||||
# ============================================
|
||||
# Verification
|
||||
# ============================================
|
||||
verify_migration() {
|
||||
log_info "Verifying migration..."
|
||||
local source_base=$(get_base_url "$SOURCE_URL")
|
||||
local dest_base=$(get_base_url "$DEST_URL")
|
||||
local schema=$(get_schema_from_url "$SOURCE_URL")
|
||||
|
||||
log_info "=== VERIFICATION ==="
|
||||
|
||||
echo ""
|
||||
echo "====== SOURCE DATABASE ROW COUNTS ======"
|
||||
psql "${SOURCE_URL}" -f "${SCRIPT_DIR}/verify_migration.sql"
|
||||
echo "Source counts:"
|
||||
psql "${source_base}" -c "SELECT 'User' as table_name, COUNT(*) FROM ${schema}.\"User\" UNION ALL SELECT 'AgentGraph', COUNT(*) FROM ${schema}.\"AgentGraph\" UNION ALL SELECT 'Profile', COUNT(*) FROM ${schema}.\"Profile\""
|
||||
|
||||
echo ""
|
||||
echo "====== DESTINATION DATABASE ROW COUNTS ======"
|
||||
psql "${DEST_URL}" -f "${SCRIPT_DIR}/verify_migration.sql"
|
||||
|
||||
echo ""
|
||||
log_info "Compare the counts above to verify migration success"
|
||||
echo "Destination counts:"
|
||||
psql "${dest_base}" -c "SELECT 'User' as table_name, COUNT(*) FROM ${schema}.\"User\" UNION ALL SELECT 'AgentGraph', COUNT(*) FROM ${schema}.\"AgentGraph\" UNION ALL SELECT 'Profile', COUNT(*) FROM ${schema}.\"Profile\""
|
||||
}
|
||||
|
||||
# Print summary
|
||||
print_summary() {
|
||||
local backup_file="$1"
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo " MIGRATION COMPLETE"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
log_success "Database has been migrated successfully"
|
||||
echo ""
|
||||
echo "Backup file: ${backup_file}"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. Verify the row counts above match"
|
||||
echo " 2. Update your application's DATABASE_URL to point to destination"
|
||||
echo " 3. Restart your application services"
|
||||
echo " 4. Test login with existing user credentials"
|
||||
echo " 5. Keep source database available for 48 hours as backup"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Main execution
|
||||
# ============================================
|
||||
# Main
|
||||
# ============================================
|
||||
main() {
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo " Database Migration Script"
|
||||
echo " Database Migration Script"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
parse_args "$@"
|
||||
|
||||
# Mask passwords in output
|
||||
local source_display="${SOURCE_URL%%:*}://****@${SOURCE_URL#*@}"
|
||||
local dest_display="${DEST_URL%%:*}://****@${DEST_URL#*@}"
|
||||
|
||||
log_info "Source: ${source_display}"
|
||||
log_info "Destination: ${dest_display}"
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
log_warn "DRY RUN MODE - No changes will be made"
|
||||
fi
|
||||
log_info "Source: $(get_base_url "$SOURCE_URL")"
|
||||
log_info "Destination: $(get_base_url "$DEST_URL")"
|
||||
[[ "$DRY_RUN" == true ]] && log_warn "DRY RUN MODE"
|
||||
echo ""
|
||||
|
||||
check_prerequisites
|
||||
test_connections
|
||||
|
||||
echo ""
|
||||
|
||||
# Step 0: Nuke destination database (with confirmation)
|
||||
nuke_destination
|
||||
echo ""
|
||||
|
||||
if [[ "$DRY_RUN" != true ]]; then
|
||||
echo ""
|
||||
log_warn "This will perform a database migration."
|
||||
log_warn "Ensure application services are stopped to prevent data loss."
|
||||
echo ""
|
||||
log_warn "This will migrate data to the destination database."
|
||||
read -p "Continue with migration? (y/N) " -n 1 -r
|
||||
echo ""
|
||||
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
log_info "Migration cancelled"
|
||||
exit 0
|
||||
fi
|
||||
[[ ! $REPLY =~ ^[Yy]$ ]] && { log_info "Cancelled"; exit 0; }
|
||||
fi
|
||||
|
||||
echo ""
|
||||
log_info "Starting migration at $(date)"
|
||||
echo ""
|
||||
|
||||
# Step 1: Auth migration on source
|
||||
run_auth_migration
|
||||
# Step 1: Export platform data (READ-ONLY on source)
|
||||
platform_file=$(export_platform_data)
|
||||
echo ""
|
||||
|
||||
# Step 2: Export from source
|
||||
backup_file=$(export_database)
|
||||
# Step 2: Export auth data (READ-ONLY on source)
|
||||
auth_file=$(export_auth_data)
|
||||
echo ""
|
||||
|
||||
# Step 3: Set up destination schema
|
||||
setup_dest_schema
|
||||
# Step 3: Import platform data to destination
|
||||
import_platform_data "$platform_file"
|
||||
echo ""
|
||||
|
||||
# Step 4: Import to destination
|
||||
import_database "$backup_file"
|
||||
# Step 4: Update User table with auth data
|
||||
update_user_auth_data "$auth_file"
|
||||
echo ""
|
||||
|
||||
# Step 5: Post-import tasks
|
||||
# Step 5: Refresh materialized views
|
||||
refresh_views
|
||||
echo ""
|
||||
|
||||
# Step 6: Verify
|
||||
# Verification
|
||||
verify_migration
|
||||
|
||||
# Summary
|
||||
if [[ "$DRY_RUN" != true ]]; then
|
||||
print_summary "$backup_file"
|
||||
else
|
||||
echo ""
|
||||
log_info "DRY RUN COMPLETE - No changes were made"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
log_info "Finished at $(date)"
|
||||
echo ""
|
||||
log_success "Migration completed at $(date)"
|
||||
echo ""
|
||||
echo "Files created:"
|
||||
echo " - Platform data: ${platform_file}"
|
||||
[[ -n "$auth_file" ]] && echo " - Auth data: ${auth_file}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main "$@"
|
||||
|
||||
Reference in New Issue
Block a user