This is an automated email from the ASF dual-hosted git repository.

skrawcz pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hamilton.git

commit 98ff08037a9411aacf820a657e0a476e20067c1e
Author: Stefan Krawczyk <[email protected]>
AuthorDate: Tue Feb 24 17:32:10 2026 -0800

    Add PostgreSQL 12→16 migration scripts for Hamilton UI
    
    Hamilton UI now requires PostgreSQL 16 (upgraded from 12) to support
    Django 6.0.2+, which requires PostgreSQL 14+.
    
    This commit adds three migration strategies:
    
    1. **migrate_postgres_simple.sh** (RECOMMENDED)
       - Uses Django's dumpdata/loaddata for safe migration
       - Handles Hamilton-specific Django apps correctly
       - Validates data before and after migration
       - Better error checking and recovery instructions
    
    2. **backup_hamilton_data.sh** & **restore_hamilton_data.sh**
       - Standalone backup/restore using Django tools
       - Can be used independently for backups
       - Exports to portable JSON format
    
    3. **migrate_postgres.sh** (Alternative approach)
       - Uses pg_dump/pg_restore for raw SQL migration
       - Provided for users who prefer PostgreSQL native tools
    
    Django approach benefits:
    - Only exports Hamilton application data (not system tables)
    - Handles foreign keys and permissions correctly
    - Portable JSON format works across PostgreSQL versions
    - Built-in validation via Django ORM
    
    Also adds UPGRADE.md documenting:
    - When migration is needed
    - Fresh start vs. data migration options
    - Troubleshooting common issues
    - Version compatibility matrix
    
    Closes backward compatibility concerns for PostgreSQL upgrade.
---
 ui/UPGRADE.md                 | 275 ++++++++++++++++++++++++++++++++++++++++++
 ui/backup_hamilton_data.sh    | 125 +++++++++++++++++++
 ui/migrate_postgres.sh        | 145 ++++++++++++++++++++++
 ui/migrate_postgres_simple.sh | 246 +++++++++++++++++++++++++++++++++++++
 ui/restore_hamilton_data.sh   | 195 ++++++++++++++++++++++++++++++
 5 files changed, 986 insertions(+)

diff --git a/ui/UPGRADE.md b/ui/UPGRADE.md
new file mode 100644
index 00000000..e177c988
--- /dev/null
+++ b/ui/UPGRADE.md
@@ -0,0 +1,275 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+# Hamilton UI Upgrade Guide
+
+This guide covers backward compatibility concerns and migration steps when 
upgrading Hamilton UI.
+
+## Upgrading from PostgreSQL 12 to PostgreSQL 16
+
+**Affected versions:** Upgrading to versions after commit `0da07178` (February 
2026)
+
+### What Changed
+
+Hamilton UI Docker setup now uses **PostgreSQL 16** instead of PostgreSQL 12 
to support Django 6.0.2+, which requires PostgreSQL 14 or later.
+
+### Do I Need to Migrate?
+
+**You need to migrate if ALL of the following are true:**
+1. You have been using Hamilton UI with Docker (`./run.sh` or `./dev.sh`)
+2. You have existing project data, runs, or artifacts you want to keep
+3. You are upgrading from a version that used PostgreSQL 12
+
+**You do NOT need to migrate if:**
+- This is a fresh installation
+- You don't care about preserving existing data
+- You're using the PyPI package (`hamilton ui`) which uses SQLite by default
+
+### Migration Options
+
+#### Option 1: Fresh Start (Recommended for Development)
+
+If your data is not critical (development/testing), the simplest approach is 
to start fresh:
+
+```bash
+cd hamilton/ui
+
+# Stop containers
+./stop.sh
+
+# Remove old PostgreSQL 12 data
+docker volume rm ui_postgres_data
+
+# Start with PostgreSQL 16
+./run.sh --build
+```
+
+**⚠️ Warning:** This will delete all existing projects, runs, and artifacts.
+
+#### Option 2: Migrate Your Data
+
+If you need to preserve your data, follow these steps:
+
+##### Step 1: Export data from PostgreSQL 12
+
+While still using the old version:
+
+```bash
+cd hamilton/ui
+
+# Ensure containers are running
+docker compose up -d
+
+# Export data
+docker compose exec db pg_dump -U hamilton hamilton > hamilton_backup.sql
+
+# Stop containers
+docker compose down
+```
+
+##### Step 2: Upgrade to PostgreSQL 16
+
+```bash
+# Remove old PostgreSQL 12 volume
+docker volume rm ui_postgres_data
+
+# Pull latest code with PostgreSQL 16
+git pull  # or checkout the latest version
+
+# Start new containers
+./run.sh --build
+```
+
+##### Step 3: Import data into PostgreSQL 16
+
+```bash
+# Wait for database to be ready
+sleep 10
+
+# Import data
+docker compose exec -T db psql -U hamilton hamilton < hamilton_backup.sql
+
+# Verify data
+docker compose exec db psql -U hamilton hamilton -c "\dt"
+```
+
+##### Step 4: Verify Migration
+
+Navigate to http://localhost:8242 and verify:
+- Your projects exist
+- Run history is preserved
+- Artifacts are accessible
+
+##### Troubleshooting
+
+**Issue:** Import fails with "relation already exists"
+
+The Hamilton UI automatically runs migrations on startup, which creates empty 
tables. Your import will skip those errors - this is normal.
+
+**Issue:** Permission errors on import
+
+```bash
+# Grant permissions to hamilton user
+docker compose exec db psql -U hamilton hamilton -c "GRANT ALL PRIVILEGES ON 
ALL TABLES IN SCHEMA public TO hamilton;"
+docker compose exec db psql -U hamilton hamilton -c "GRANT ALL PRIVILEGES ON 
ALL SEQUENCES IN SCHEMA public TO hamilton;"
+```
+
+**Issue:** Blob/artifact files missing
+
+If you stored artifacts locally (not S3), you also need to copy the blob 
storage:
+
+```bash
+# Identify blob storage location (check docker-compose.yml)
+docker compose exec backend ls /code/blobs
+
+# If using local storage, copy blobs directory
+cp -r old_blobs_directory new_blobs_directory
+```
+
+#### Option 3: Use pg_upgrade (Advanced)
+
+For minimal downtime migrations, you can use PostgreSQL's `pg_upgrade` tool. 
This is more complex and generally not recommended for Hamilton UI due to its 
development-focused nature.
+
+See [PostgreSQL pg_upgrade 
documentation](https://www.postgresql.org/docs/current/pgupgrade.html) for 
details.
+
+## Other Breaking Changes
+
+### Python Version (Internal)
+
+The Docker images now use **Python 3.12** instead of Python 3.8. This is 
transparent to users - no action needed.
+
+**Why:** Python 3.8 reached end-of-life in October 2024. Python 3.12 provides 
better performance and security.
+
+### Dependency Management (Internal)
+
+The Docker backend now uses **uv** instead of pip for dependency management. 
This is transparent to users - no action needed.
+
+**Why:** uv provides faster installs and reproducible builds via lock files.
+
+### Django 6.0+ Requirements
+
+Django 6.0.2+ requires PostgreSQL 14 or later, which is why we upgraded to 
PostgreSQL 16.
+
+## Migration Script
+
+For automated migration, you can use this script:
+
+```bash
+#!/bin/bash
+# migrate_postgres.sh - Migrate Hamilton UI from PostgreSQL 12 to 16
+
+set -e
+
+BACKUP_FILE="hamilton_backup_$(date +%Y%m%d_%H%M%S).sql"
+
+echo "Hamilton UI PostgreSQL 12 → 16 Migration"
+echo "========================================"
+echo ""
+
+# Check if old containers are running
+if ! docker compose ps | grep -q "ui-db"; then
+    echo "Error: Hamilton UI containers not running. Start them first with 
./run.sh"
+    exit 1
+fi
+
+# Backup
+echo "Step 1: Backing up PostgreSQL 12 data..."
+docker compose exec -T db pg_dump -U hamilton hamilton > "$BACKUP_FILE"
+echo "✓ Backup saved to: $BACKUP_FILE"
+echo ""
+
+# Stop and remove
+echo "Step 2: Stopping containers and removing old data..."
+docker compose down
+docker volume rm ui_postgres_data
+echo "✓ Old data removed"
+echo ""
+
+# Start new
+echo "Step 3: Starting PostgreSQL 16..."
+./run.sh --build
+sleep 15  # Wait for initialization
+echo "✓ PostgreSQL 16 ready"
+echo ""
+
+# Restore
+echo "Step 4: Restoring data..."
+docker compose exec -T db psql -U hamilton hamilton < "$BACKUP_FILE" 2>&1 | 
grep -v "ERROR:.*already exists" || true
+echo "✓ Data restored"
+echo ""
+
+# Fix permissions
+echo "Step 5: Fixing permissions..."
+docker compose exec -T db psql -U hamilton hamilton -c "GRANT ALL PRIVILEGES 
ON ALL TABLES IN SCHEMA public TO hamilton;" > /dev/null
+docker compose exec -T db psql -U hamilton hamilton -c "GRANT ALL PRIVILEGES 
ON ALL SEQUENCES IN SCHEMA public TO hamilton;" > /dev/null
+echo "✓ Permissions fixed"
+echo ""
+
+echo "Migration complete!"
+echo "Navigate to http://localhost:8242 to verify your data."
+```
+
+Save as `migrate_postgres.sh`, make executable with `chmod +x 
migrate_postgres.sh`, and run with `./migrate_postgres.sh`.
+
+## Getting Help
+
+If you encounter issues during migration:
+
+1. Check the [Hamilton UI 
documentation](https://hamilton.apache.org/concepts/ui)
+2. Search or create an issue on 
[GitHub](https://github.com/apache/hamilton/issues)
+3. Join our [Slack 
community](https://join.slack.com/t/hamilton-opensource/shared_invite/zt-2niepkra8-DGKGf_tTYhXuJWBTXtIs4g)
+
+## Version Compatibility Matrix
+
+| Hamilton UI Version | PostgreSQL Version | Python Version | Django Version |
+|--------------------|--------------------|----------------|----------------|
+| < 0.0.17 (2026-02) | 12 | 3.8 | 4.2 |
+| ≥ 0.0.17 (2026-02) | 16 | 3.12 | 6.0.2 |
+
+## FAQ
+
+**Q: Can I continue using PostgreSQL 12?**
+
+No. Django 6.0.2+ explicitly requires PostgreSQL 14 or later. You must upgrade 
to at least PostgreSQL 14 (we recommend 16).
+
+**Q: Will my PyPI installation be affected?**
+
+No. The PyPI package (`apache-hamilton-ui`) uses SQLite by default for local 
development. This upgrade only affects Docker deployments.
+
+**Q: Can I downgrade if something goes wrong?**
+
+Yes, as long as you keep your backup file:
+
+```bash
+# Checkout old version
+git checkout <old_commit_before_upgrade>
+
+# Remove new volume
+docker volume rm ui_postgres_data
+
+# Start old version
+./run.sh --build
+
+# Restore from backup (if needed)
+docker compose exec -T db psql -U hamilton hamilton < hamilton_backup.sql
+```
+
+**Q: Do I need to update my Hamilton SDK client code?**
+
+No. The Hamilton SDK (`apache-hamilton-sdk`) is compatible with both old and 
new versions of the UI backend. No client code changes required.
diff --git a/ui/backup_hamilton_data.sh b/ui/backup_hamilton_data.sh
new file mode 100755
index 00000000..87e601e3
--- /dev/null
+++ b/ui/backup_hamilton_data.sh
@@ -0,0 +1,125 @@
+#!/bin/bash
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# backup_hamilton_data.sh - Export Hamilton UI data using Django's dumpdata
+
+set -euo pipefail
+
+BACKUP_FILE="${1:-hamilton_data_$(date +%Y%m%d_%H%M%S).json}"
+
+echo "Hamilton UI Data Backup"
+echo "======================="
+echo ""
+
+# Check if docker compose or docker-compose is available
+if command -v docker-compose &> /dev/null; then
+    DOCKER_COMPOSE="docker-compose"
+elif docker compose version &> /dev/null 2>&1; then
+    DOCKER_COMPOSE="docker compose"
+else
+    echo "Error: Neither 'docker compose' nor 'docker-compose' found"
+    exit 1
+fi
+
+# Check if containers are running
+if ! $DOCKER_COMPOSE ps --services --filter "status=running" | grep -q 
"backend"; then
+    echo "Error: Hamilton UI backend container is not running"
+    echo "Start containers first: ./run.sh or ./dev.sh"
+    exit 1
+fi
+
+# Check if database is ready
+echo "Checking database connection..."
+if ! $DOCKER_COMPOSE exec -T backend python manage.py migrate --check > 
/dev/null 2>&1; then
+    echo "Error: Database is not ready or migrations are pending"
+    echo "Run: $DOCKER_COMPOSE exec backend python manage.py migrate"
+    exit 1
+fi
+echo "✓ Database connected"
+echo ""
+
+# List Hamilton UI Django apps (excluding Django system apps)
+HAMILTON_APPS=(
+    "trackingserver_base"
+    "trackingserver_auth"
+    "trackingserver_projects"
+    "trackingserver_run_tracking"
+    "trackingserver_template"
+)
+
+echo "Backing up Hamilton UI data..."
+echo "Apps: ${HAMILTON_APPS[*]}"
+echo ""
+
+# Dump data using Django's dumpdata
+# This is better than pg_dump because:
+# - Handles foreign keys correctly
+# - Portable JSON format (works across PostgreSQL versions)
+# - Only dumps application data (not system tables)
+# - Django handles restore ordering automatically
+if ! $DOCKER_COMPOSE exec -T backend python manage.py dumpdata \
+    --natural-foreign \
+    --natural-primary \
+    --indent 2 \
+    "${HAMILTON_APPS[@]}" > "$BACKUP_FILE"; then
+    echo "Error: Backup failed"
+    rm -f "$BACKUP_FILE"
+    exit 1
+fi
+
+# Validate backup file
+if [ ! -s "$BACKUP_FILE" ]; then
+    echo "Error: Backup file is empty"
+    rm -f "$BACKUP_FILE"
+    exit 1
+fi
+
+# Check if it's valid JSON
+if ! python3 -m json.tool "$BACKUP_FILE" > /dev/null 2>&1; then
+    echo "Error: Backup file contains invalid JSON"
+    exit 1
+fi
+
+# Count records
+RECORD_COUNT=$(python3 -c "import json; 
print(len(json.load(open('$BACKUP_FILE'))))")
+FILE_SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
+
+echo "✓ Backup complete"
+echo ""
+echo "File: $BACKUP_FILE"
+echo "Size: $FILE_SIZE"
+echo "Records: $RECORD_COUNT"
+echo ""
+
+# Show breakdown by model
+echo "Records by model:"
+python3 -c "
+import json
+from collections import Counter
+
+data = json.load(open('$BACKUP_FILE'))
+models = Counter(item['model'] for item in data)
+
+for model, count in sorted(models.items()):
+    print(f'  {model}: {count}')
+"
+
+echo ""
+echo "To restore this backup on a new system:"
+echo "  1. Start Hamilton UI: ./run.sh"
+echo "  2. Run: ./restore_hamilton_data.sh $BACKUP_FILE"
diff --git a/ui/migrate_postgres.sh b/ui/migrate_postgres.sh
new file mode 100755
index 00000000..7bfa734f
--- /dev/null
+++ b/ui/migrate_postgres.sh
@@ -0,0 +1,145 @@
+#!/bin/bash
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# migrate_postgres.sh - Migrate Hamilton UI from PostgreSQL 12 to 16
+
+set -e
+
+BACKUP_FILE="hamilton_backup_$(date +%Y%m%d_%H%M%S).sql"
+
+echo "Hamilton UI PostgreSQL 12 → 16 Migration"
+echo "========================================"
+echo ""
+echo "This script will:"
+echo "  1. Backup your PostgreSQL 12 data"
+echo "  2. Stop containers and remove old data volume"
+echo "  3. Start PostgreSQL 16 containers"
+echo "  4. Restore your data to PostgreSQL 16"
+echo ""
+read -p "Continue? (yes/no): " CONFIRM
+
+if [ "$CONFIRM" != "yes" ]; then
+    echo "Aborted."
+    exit 0
+fi
+
+# Check if docker compose or docker-compose is available
+if command -v docker-compose &> /dev/null; then
+    DOCKER_COMPOSE="docker-compose"
+elif docker compose version &> /dev/null; then
+    DOCKER_COMPOSE="docker compose"
+else
+    echo "Error: Neither 'docker compose' nor 'docker-compose' command found"
+    exit 1
+fi
+
+# Check if old containers are running
+if ! $DOCKER_COMPOSE ps | grep -q "db"; then
+    echo "Error: Hamilton UI containers not running. Start them first with 
./run.sh"
+    exit 1
+fi
+
+# Check PostgreSQL version
+PG_VERSION=$($DOCKER_COMPOSE exec -T db psql -U hamilton -d hamilton -c "SHOW 
server_version;" -t | tr -d ' ')
+if [[ $PG_VERSION == 16* ]]; then
+    echo "You are already running PostgreSQL 16. No migration needed."
+    exit 0
+fi
+
+echo ""
+echo "Detected PostgreSQL version: $PG_VERSION"
+echo ""
+
+# Backup
+echo "Step 1: Backing up PostgreSQL data..."
+$DOCKER_COMPOSE exec -T db pg_dump -U hamilton hamilton > "$BACKUP_FILE"
+echo "✓ Backup saved to: $BACKUP_FILE ($(du -h "$BACKUP_FILE" | cut -f1))"
+echo ""
+
+# Stop and remove
+echo "Step 2: Stopping containers and removing old data..."
+$DOCKER_COMPOSE down
+docker volume rm ui_postgres_data || true
+echo "✓ Old data removed"
+echo ""
+
+# Start new
+echo "Step 3: Starting PostgreSQL 16 containers..."
+echo "   This may take a few minutes on first run..."
+./run.sh --build > /tmp/hamilton_build.log 2>&1 &
+BUILD_PID=$!
+
+# Wait for build to complete
+wait $BUILD_PID || true
+
+# Wait for database to be ready
+echo "   Waiting for database to initialize..."
+sleep 20
+
+MAX_RETRIES=30
+RETRY_COUNT=0
+until $DOCKER_COMPOSE exec -T db pg_isready -U hamilton > /dev/null 2>&1; do
+    RETRY_COUNT=$((RETRY_COUNT + 1))
+    if [ $RETRY_COUNT -ge $MAX_RETRIES ]; then
+        echo "Error: Database failed to start after $MAX_RETRIES attempts"
+        echo "Check logs: $DOCKER_COMPOSE logs db"
+        exit 1
+    fi
+    echo "   Still waiting... ($RETRY_COUNT/$MAX_RETRIES)"
+    sleep 2
+done
+
+echo "✓ PostgreSQL 16 ready"
+echo ""
+
+# Restore
+echo "Step 4: Restoring data to PostgreSQL 16..."
+echo "   (Ignoring 'already exists' errors - this is normal)"
+$DOCKER_COMPOSE exec -T db psql -U hamilton hamilton < "$BACKUP_FILE" 2>&1 | \
+    grep -v "ERROR:.*already exists" | \
+    grep -v "^$" || true
+echo "✓ Data restored"
+echo ""
+
+# Fix permissions
+echo "Step 5: Fixing permissions..."
+$DOCKER_COMPOSE exec -T db psql -U hamilton hamilton -c "GRANT ALL PRIVILEGES 
ON ALL TABLES IN SCHEMA public TO hamilton;" > /dev/null
+$DOCKER_COMPOSE exec -T db psql -U hamilton hamilton -c "GRANT ALL PRIVILEGES 
ON ALL SEQUENCES IN SCHEMA public TO hamilton;" > /dev/null
+echo "✓ Permissions fixed"
+echo ""
+
+# Verify
+echo "Step 6: Verifying migration..."
+TABLE_COUNT=$($DOCKER_COMPOSE exec -T db psql -U hamilton hamilton -c "\dt" | 
grep -c "public" || echo "0")
+echo "✓ Found $TABLE_COUNT tables"
+echo ""
+
+echo "========================================"
+echo "Migration complete!"
+echo "========================================"
+echo ""
+echo "Next steps:"
+echo "  1. Navigate to http://localhost:8242";
+echo "  2. Verify your projects and runs are present"
+echo "  3. If everything looks good, you can delete the backup:"
+echo "     rm $BACKUP_FILE"
+echo ""
+echo "If you encounter issues:"
+echo "  - Check logs: $DOCKER_COMPOSE logs"
+echo "  - Restore from backup: See UPGRADE.md"
+echo "  - Get help: https://github.com/apache/hamilton/issues";
diff --git a/ui/migrate_postgres_simple.sh b/ui/migrate_postgres_simple.sh
new file mode 100755
index 00000000..4acd60c5
--- /dev/null
+++ b/ui/migrate_postgres_simple.sh
@@ -0,0 +1,246 @@
+#!/bin/bash
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# migrate_postgres_simple.sh - Migrate Hamilton UI from PostgreSQL 12 to 16
+# Uses Django's dumpdata/loaddata for safe, validated migration
+
+set -euo pipefail
+
+BACKUP_FILE="hamilton_migration_$(date +%Y%m%d_%H%M%S).json"
+
+echo "Hamilton UI PostgreSQL 12 → 16 Migration"
+echo "========================================="
+echo ""
+echo "This script will:"
+echo "  1. Export your Hamilton data using Django"
+echo "  2. Stop containers and remove PostgreSQL 12 volume"
+echo "  3. Start PostgreSQL 16 containers"
+echo "  4. Restore your Hamilton data"
+echo ""
+
+# Check if docker compose or docker-compose is available
+if command -v docker-compose &> /dev/null; then
+    DOCKER_COMPOSE="docker-compose"
+elif docker compose version &> /dev/null 2>&1; then
+    DOCKER_COMPOSE="docker compose"
+else
+    echo "Error: Neither 'docker compose' nor 'docker-compose' found"
+    exit 1
+fi
+
+# Check if containers are running
+if ! $DOCKER_COMPOSE ps --services --filter "status=running" | grep -q 
"backend"; then
+    echo "Error: Hamilton UI containers not running"
+    echo "Start them first: ./run.sh"
+    exit 1
+fi
+
+# Check PostgreSQL version
+echo "Checking current PostgreSQL version..."
+PG_VERSION=$($DOCKER_COMPOSE exec -T db psql -U hamilton -d hamilton -At -c 
"SHOW server_version;" | cut -d. -f1)
+
+if [ "$PG_VERSION" -ge 16 ]; then
+    echo "Already on PostgreSQL $PG_VERSION. No migration needed."
+    exit 0
+fi
+
+if [ "$PG_VERSION" -lt 12 ]; then
+    echo "Error: PostgreSQL $PG_VERSION detected"
+    echo "This script handles PostgreSQL 12+ → 16"
+    echo "Manual upgrade required for older versions"
+    exit 1
+fi
+
+echo "Current version: PostgreSQL $PG_VERSION"
+echo ""
+
+read -p "Continue with migration? (yes/no): " CONFIRM
+if [ "$CONFIRM" != "yes" ]; then
+    echo "Aborted."
+    exit 0
+fi
+
+echo ""
+echo "================================================================"
+echo "Step 1: Exporting Hamilton data"
+echo "================================================================"
+echo ""
+
+# Use the backup script
+if ! ./backup_hamilton_data.sh "$BACKUP_FILE"; then
+    echo "Error: Data export failed"
+    exit 1
+fi
+
+# Verify backup was created and has content
+if [ ! -s "$BACKUP_FILE" ]; then
+    echo "Error: Backup file is empty or missing"
+    exit 1
+fi
+
+RECORD_COUNT=$(python3 -c "import json; 
print(len(json.load(open('$BACKUP_FILE'))))")
+if [ "$RECORD_COUNT" -eq 0 ]; then
+    echo "Warning: Backup contains 0 records"
+    echo "This is OK if you have no data yet, but unexpected if you've been 
using Hamilton UI"
+    read -p "Continue anyway? (yes/no): " CONTINUE
+    if [ "$CONTINUE" != "yes" ]; then
+        echo "Aborted. Backup saved at: $BACKUP_FILE"
+        exit 0
+    fi
+fi
+
+echo ""
+echo "================================================================"
+echo "Step 2: Removing PostgreSQL 12 data"
+echo "================================================================"
+echo ""
+echo "⚠️  WARNING: About to delete PostgreSQL 12 data volume"
+echo "   Backup: $BACKUP_FILE ($RECORD_COUNT records)"
+echo ""
+read -p "Type 'DELETE' to confirm: " CONFIRM_DELETE
+
+if [ "$CONFIRM_DELETE" != "DELETE" ]; then
+    echo "Aborted. Your backup is saved at: $BACKUP_FILE"
+    exit 0
+fi
+
+echo ""
+echo "Stopping containers..."
+$DOCKER_COMPOSE down
+
+echo "Removing PostgreSQL 12 volume..."
+if docker volume rm ui_postgres_data; then
+    echo "✓ Volume removed"
+else
+    echo "Error: Could not remove volume"
+    echo "It may not exist or be in use by another container"
+    docker volume ls | grep postgres
+    exit 1
+fi
+
+echo ""
+echo "================================================================"
+echo "Step 3: Starting PostgreSQL 16"
+echo "================================================================"
+echo ""
+
+if [ ! -f "./run.sh" ]; then
+    echo "Error: ./run.sh not found"
+    echo "Run this script from the ui/ directory"
+    exit 1
+fi
+
+echo "Building and starting containers..."
+echo "(This may take a few minutes on first build)"
+echo ""
+
+if ! ./run.sh --build; then
+    echo ""
+    echo "Error: Failed to start containers"
+    echo "Your backup is safe at: $BACKUP_FILE"
+    echo ""
+    echo "To retry manually:"
+    echo "  1. Fix any Docker issues"
+    echo "  2. Run: ./run.sh --build"
+    echo "  3. Run: ./restore_hamilton_data.sh $BACKUP_FILE"
+    exit 1
+fi
+
+# Wait for backend to be ready
+echo ""
+echo "Waiting for backend to be ready..."
+MAX_RETRIES=30
+RETRY_COUNT=0
+
+until $DOCKER_COMPOSE exec -T backend python manage.py migrate --check > 
/dev/null 2>&1; do
+    RETRY_COUNT=$((RETRY_COUNT + 1))
+    if [ $RETRY_COUNT -ge $MAX_RETRIES ]; then
+        echo "Error: Backend not ready after $MAX_RETRIES attempts"
+        echo "Your backup is safe at: $BACKUP_FILE"
+        echo ""
+        echo "To restore manually:"
+        echo "  1. Wait for containers to be healthy: $DOCKER_COMPOSE ps"
+        echo "  2. Run: ./restore_hamilton_data.sh $BACKUP_FILE"
+        exit 1
+    fi
+    echo "  Waiting... (attempt $RETRY_COUNT/$MAX_RETRIES)"
+    sleep 2
+done
+
+echo "✓ Backend ready"
+
+# Check new PostgreSQL version
+NEW_PG_VERSION=$($DOCKER_COMPOSE exec -T db psql -U hamilton -d hamilton -At 
-c "SHOW server_version;" | cut -d. -f1)
+echo "✓ PostgreSQL $NEW_PG_VERSION running"
+
+echo ""
+echo "================================================================"
+echo "Step 4: Restoring Hamilton data"
+echo "================================================================"
+echo ""
+
+# Copy backup into backend container so loaddata can access it
+echo "Copying backup into container..."
+$DOCKER_COMPOSE cp "$BACKUP_FILE" backend:/tmp/restore.json
+
+# Use the restore script
+if ! $DOCKER_COMPOSE exec -T backend bash -c "cd /code && python manage.py 
loaddata /tmp/restore.json"; then
+    echo ""
+    echo "Error: Data restore failed"
+    echo "Your backup is safe at: $BACKUP_FILE"
+    echo ""
+    echo "To retry manually:"
+    echo "  ./restore_hamilton_data.sh $BACKUP_FILE"
+    exit 1
+fi
+
+echo "✓ Data restored"
+
+# Verify
+echo ""
+echo "Verifying migration..."
+POST_COUNTS=$($DOCKER_COMPOSE exec -T backend python manage.py shell <<'PYTHON'
+from trackingserver_base.models import Project
+from trackingserver_run_tracking.models import DAGRun
+
+print(f"{Project.objects.count()},{DAGRun.objects.count()}")
+PYTHON
+)
+
+IFS=',' read -r PROJECTS RUNS <<< "$POST_COUNTS"
+
+echo "  Projects: $PROJECTS"
+echo "  Runs: $RUNS"
+
+echo ""
+echo "================================================================"
+echo "✅ Migration Complete!"
+echo "================================================================"
+echo ""
+echo "Your Hamilton UI has been upgraded from PostgreSQL $PG_VERSION to 
$NEW_PG_VERSION"
+echo ""
+echo "Next steps:"
+echo "  1. Navigate to http://localhost:8242";
+echo "  2. Verify your projects and runs appear"
+echo "  3. If everything looks good, delete the backup:"
+echo "     rm $BACKUP_FILE"
+echo ""
+echo "If you encounter issues:"
+echo "  - Backup file: $BACKUP_FILE"
+echo "  - Check logs: $DOCKER_COMPOSE logs"
+echo "  - Get help: https://github.com/apache/hamilton/issues";
diff --git a/ui/restore_hamilton_data.sh b/ui/restore_hamilton_data.sh
new file mode 100755
index 00000000..dd045c4a
--- /dev/null
+++ b/ui/restore_hamilton_data.sh
@@ -0,0 +1,195 @@
+#!/bin/bash
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# restore_hamilton_data.sh - Import Hamilton UI data using Django's loaddata
+
+set -euo pipefail
+
+BACKUP_FILE="${1:-}"
+
+if [ -z "$BACKUP_FILE" ]; then
+    echo "Usage: $0 <backup_file.json>"
+    echo ""
+    echo "Available backups:"
+    ls -lh hamilton_data_*.json 2>/dev/null || echo "  (none found)"
+    exit 1
+fi
+
+if [ ! -f "$BACKUP_FILE" ]; then
+    echo "Error: Backup file not found: $BACKUP_FILE"
+    exit 1
+fi
+
+echo "Hamilton UI Data Restore"
+echo "========================"
+echo ""
+echo "Backup file: $BACKUP_FILE"
+echo "Size: $(du -h "$BACKUP_FILE" | cut -f1)"
+echo ""
+
+# Validate JSON
+echo "Validating backup file..."
+if ! python3 -m json.tool "$BACKUP_FILE" > /dev/null 2>&1; then
+    echo "Error: Backup file contains invalid JSON"
+    exit 1
+fi
+
+RECORD_COUNT=$(python3 -c "import json; 
print(len(json.load(open('$BACKUP_FILE'))))")
+echo "✓ Found $RECORD_COUNT records"
+echo ""
+
+# Check if docker compose or docker-compose is available
+if command -v docker-compose &> /dev/null; then
+    DOCKER_COMPOSE="docker-compose"
+elif docker compose version &> /dev/null 2>&1; then
+    DOCKER_COMPOSE="docker compose"
+else
+    echo "Error: Neither 'docker compose' nor 'docker-compose' found"
+    exit 1
+fi
+
+# Check if containers are running
+if ! $DOCKER_COMPOSE ps --services --filter "status=running" | grep -q 
"backend"; then
+    echo "Error: Hamilton UI backend container is not running"
+    echo "Start containers first: ./run.sh"
+    exit 1
+fi
+
+# Check if database is ready
+echo "Checking database connection..."
+if ! $DOCKER_COMPOSE exec -T backend python manage.py migrate --check > 
/dev/null 2>&1; then
+    echo "Error: Database is not ready or migrations need to run"
+    echo "This is normal for a fresh installation."
+    echo ""
+    echo "Running migrations..."
+    if ! $DOCKER_COMPOSE exec -T backend python manage.py migrate; then
+        echo "Error: Migrations failed"
+        exit 1
+    fi
+    echo "✓ Migrations complete"
+fi
+echo "✓ Database ready"
+echo ""
+
+# Check if there's existing data
+EXISTING_PROJECTS=$($DOCKER_COMPOSE exec -T backend python manage.py shell -c 
"from trackingserver_base.models import *; print(Project.objects.count())" 
2>/dev/null || echo "0")
+
+if [ "$EXISTING_PROJECTS" != "0" ]; then
+    echo "⚠️  WARNING: Database already contains $EXISTING_PROJECTS projects"
+    echo ""
+    echo "Restoring will ADD to existing data (not replace it)."
+    echo "This may cause conflicts if the same data exists."
+    echo ""
+    read -p "Continue with restore? (yes/no): " CONFIRM
+    if [ "$CONFIRM" != "yes" ]; then
+        echo "Aborted."
+        exit 0
+    fi
+    echo ""
+fi
+
+# Create a pre-restore snapshot of record counts
+echo "Creating pre-restore snapshot..."
+PRE_RESTORE_COUNTS=$($DOCKER_COMPOSE exec -T backend python manage.py shell 
<<'PYTHON'
+from trackingserver_base.models import Project
+from trackingserver_projects.models import ProjectVersion
+from trackingserver_run_tracking.models import DAGRun, DAGTemplate
+
+print(f"{Project.objects.count()},{ProjectVersion.objects.count()},{DAGRun.objects.count()},{DAGTemplate.objects.count()}")
+PYTHON
+)
+
+IFS=',' read -r PRE_PROJECTS PRE_VERSIONS PRE_RUNS PRE_TEMPLATES <<< 
"$PRE_RESTORE_COUNTS"
+
+echo "  Projects: $PRE_PROJECTS"
+echo "  Versions: $PRE_VERSIONS"
+echo "  Runs: $PRE_RUNS"
+echo "  Templates: $PRE_TEMPLATES"
+echo ""
+
+# Restore data using Django's loaddata
+echo "Restoring data..."
+RESTORE_LOG="/tmp/hamilton_restore_$(date +%Y%m%d_%H%M%S).log"
+
+if ! $DOCKER_COMPOSE exec -T backend python manage.py loaddata "$BACKUP_FILE" 
> "$RESTORE_LOG" 2>&1; then
+    echo "Error: Restore failed"
+    echo ""
+    echo "Log file: $RESTORE_LOG"
+    echo ""
+    echo "Last 20 lines of error:"
+    tail -20 "$RESTORE_LOG"
+    exit 1
+fi
+
+echo "✓ Data loaded"
+echo ""
+
+# Verify restore by comparing counts
+echo "Verifying restore..."
+POST_RESTORE_COUNTS=$($DOCKER_COMPOSE exec -T backend python manage.py shell 
<<'PYTHON'
+from trackingserver_base.models import Project
+from trackingserver_projects.models import ProjectVersion
+from trackingserver_run_tracking.models import DAGRun, DAGTemplate
+
+print(f"{Project.objects.count()},{ProjectVersion.objects.count()},{DAGRun.objects.count()},{DAGTemplate.objects.count()}")
+PYTHON
+)
+
+IFS=',' read -r POST_PROJECTS POST_VERSIONS POST_RUNS POST_TEMPLATES <<< 
"$POST_RESTORE_COUNTS"
+
+ADDED_PROJECTS=$((POST_PROJECTS - PRE_PROJECTS))
+ADDED_VERSIONS=$((POST_VERSIONS - PRE_VERSIONS))
+ADDED_RUNS=$((POST_RUNS - PRE_RUNS))
+ADDED_TEMPLATES=$((POST_TEMPLATES - PRE_TEMPLATES))
+
+echo "Records added:"
+echo "  Projects: $ADDED_PROJECTS (total: $POST_PROJECTS)"
+echo "  Versions: $ADDED_VERSIONS (total: $POST_VERSIONS)"
+echo "  Runs: $ADDED_RUNS (total: $POST_RUNS)"
+echo "  Templates: $ADDED_TEMPLATES (total: $POST_TEMPLATES)"
+echo ""
+
+# Sanity check - if we expected to add data but nothing changed
+EXPECTED_RECORDS=$(python3 -c "
+import json
+data = json.load(open('$BACKUP_FILE'))
+models = [item['model'] for item in data]
+print(len([m for m in models if 'project' in m or 'dagrun' in m or 
'dagtemplate' in m]))
+")
+
+if [ "$EXPECTED_RECORDS" -gt 0 ] && [ "$ADDED_PROJECTS" -eq 0 ] && [ 
"$ADDED_RUNS" -eq 0 ]; then
+    echo "⚠️  Warning: Backup contained records but none were added"
+    echo "   This could mean:"
+    echo "   - Records already exist (duplicates skipped)"
+    echo "   - Backup is from a different schema version"
+    echo "   - There was an error during restore"
+    echo ""
+    echo "   Check the restore log: $RESTORE_LOG"
+    exit 1
+fi
+
+echo "✓ Restore complete!"
+echo ""
+echo "Next steps:"
+echo "  1. Navigate to http://localhost:8242";
+echo "  2. Verify your projects and runs appear"
+echo "  3. Check that data looks correct"
+echo ""
+echo "If something looks wrong:"
+echo "  - Check logs: $DOCKER_COMPOSE logs backend"
+echo "  - Restore log: $RESTORE_LOG"


Reply via email to