feat: enhance README with comprehensive screenshot showcase
- Add organized screenshot sections for better visual presentation - Include all 12 available screenshots from assets/screenshots/ - Group screenshots into logical categories: * Core Application Views (Dashboard, Projects, Tasks, Clients) * Management & Analytics (Reports, Visual Analytics, Task Management, Admin) * Data Entry & Creation (Log Time, New Task, New Client, New Project) - Improve visual layout with proper spacing and responsive design - Enhance user experience by showcasing full application capabilities
@@ -52,7 +52,7 @@ RUN mkdir -p /app/app/static/uploads/logos /app/static/uploads/logos && \
|
||||
COPY docker/start-fixed.py /app/start.py
|
||||
|
||||
# Make startup scripts executable
|
||||
RUN chmod +x /app/start.py /app/docker/init-database.py /app/docker/init-database-sql.py /app/docker/init-database-enhanced.py /app/docker/verify-database.py /app/docker/test-db.py /app/docker/test-routing.py
|
||||
RUN chmod +x /app/start.py /app/docker/init-database.py /app/docker/init-database-sql.py /app/docker/init-database-enhanced.py /app/docker/verify-database.py /app/docker/test-db.py /app/docker/test-routing.py /app/docker/entrypoint.sh /app/docker/entrypoint_fixed.sh /app/docker/startup_with_migration.py /app/docker/test_db_connection.py /app/docker/debug_startup.sh /app/docker/simple_test.sh
|
||||
|
||||
# Create non-root user
|
||||
RUN useradd -m -u 1000 timetracker && \
|
||||
@@ -71,5 +71,8 @@ EXPOSE 8080
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
||||
CMD curl -f http://localhost:8080/_health || exit 1
|
||||
|
||||
# Set the entrypoint
|
||||
ENTRYPOINT ["/app/docker/entrypoint_fixed.sh"]
|
||||
|
||||
# Run the application
|
||||
CMD ["python", "/app/start.py"]
|
||||
|
||||
92
README.md
@@ -4,12 +4,28 @@ A comprehensive web-based time tracking application built with Flask, featuring
|
||||
|
||||
## 📸 Screenshots
|
||||
|
||||
### Core Application Views
|
||||
<div align="center">
|
||||
<img src="assets/screenshots/Dashboard.png" alt="Dashboard" width="300" style="margin: 10px;">
|
||||
<img src="assets/screenshots/Projects.png" alt="Projects" width="300" style="margin: 10px;">
|
||||
<img src="assets/screenshots/Tasks.png" alt="Tasks" width="300" style="margin: 10px;">
|
||||
<img src="assets/screenshots/Clients.png" alt="Clients" width="300" style="margin: 10px;">
|
||||
</div>
|
||||
|
||||
### Management & Analytics
|
||||
<div align="center">
|
||||
<img src="assets/screenshots/Reports.png" alt="Reports" width="300" style="margin: 10px;">
|
||||
<img src="assets/screenshots/VisualAnalytics.png" alt="Visual Analytics" width="300" style="margin: 10px;">
|
||||
<img src="assets/screenshots/Task_Management.png" alt="Task Management" width="300" style="margin: 10px;">
|
||||
<img src="assets/screenshots/Admin.png" alt="Admin Panel" width="300" style="margin: 10px;">
|
||||
</div>
|
||||
|
||||
### Data Entry & Creation
|
||||
<div align="center">
|
||||
<img src="assets/screenshots/LogTime.png" alt="Log Time" width="300" style="margin: 10px;">
|
||||
<img src="assets/screenshots/New-Task.png" alt="New Task Creation" width="300" style="margin: 10px;">
|
||||
<img src="assets/screenshots/New-Client.png" alt="New Client Creation" width="300" style="margin: 10px;">
|
||||
<img src="assets/screenshots/New-Project.png" alt="New Project Creation" width="300" style="margin: 10px;">
|
||||
</div>
|
||||
|
||||
## 🌐 Platform Support
|
||||
@@ -110,9 +126,9 @@ A comprehensive web-based time tracking application built with Flask, featuring
|
||||
|
||||
### Import Capabilities
|
||||
- **Database Schema**: PostgreSQL and SQLite support
|
||||
- **Migration Scripts**: Automated database schema updates
|
||||
- **Migration System**: Flask-Migrate with version tracking and rollback support
|
||||
- **Backup/Restore**: Database backup and restoration tools
|
||||
- **CLI Management**: Command-line database operations
|
||||
- **CLI Management**: Command-line database operations with migration commands
|
||||
|
||||
### API Integration
|
||||
- **RESTful Endpoints**: Standard HTTP API for external access
|
||||
@@ -149,9 +165,13 @@ TimeTracker/
|
||||
├── docker/ # Docker-related scripts and utilities
|
||||
│ ├── config/ # Configuration files (Caddyfile, supervisord)
|
||||
│ ├── fixes/ # Database and permission fix scripts
|
||||
│ ├── migrations/ # Database migration scripts
|
||||
│ ├── startup/ # Startup and initialization scripts
|
||||
│ └── tests/ # Docker environment test scripts
|
||||
├── migrations/ # Database migrations with Flask-Migrate
|
||||
│ ├── versions/ # Migration version files
|
||||
│ ├── env.py # Migration environment configuration
|
||||
│ ├── script.py.mako # Migration template
|
||||
│ └── README.md # Migration documentation
|
||||
├── scripts/ # Deployment and utility scripts
|
||||
├── tests/ # Application test suite
|
||||
├── templates/ # Additional templates
|
||||
@@ -184,6 +204,72 @@ Multiple Docker configurations are available for different deployment scenarios:
|
||||
- Secure cookie settings enabled
|
||||
- Suitable for testing pre-release versions
|
||||
|
||||
### Database Migration System
|
||||
|
||||
The application now uses **Flask-Migrate** for standardized database migrations with:
|
||||
|
||||
- **Version Tracking**: Complete history of all database schema changes
|
||||
- **Rollback Support**: Ability to revert to previous database versions
|
||||
- **Automatic Schema Detection**: Migrations generated from SQLAlchemy models
|
||||
- **Cross-Database Support**: Works with both PostgreSQL and SQLite
|
||||
- **CLI Commands**: Simple commands for migration management
|
||||
|
||||
#### Migration Commands
|
||||
```bash
|
||||
# Initialize migrations (first time only)
|
||||
flask db init
|
||||
|
||||
# Create a new migration
|
||||
flask db migrate -m "Description of changes"
|
||||
|
||||
# Apply pending migrations
|
||||
flask db upgrade
|
||||
|
||||
# Rollback last migration
|
||||
flask db downgrade
|
||||
|
||||
# Check migration status
|
||||
flask db current
|
||||
|
||||
# View migration history
|
||||
flask db history
|
||||
```
|
||||
|
||||
#### Quick Migration Setup
|
||||
```bash
|
||||
# Use the migration management script
|
||||
python migrations/manage_migrations.py
|
||||
|
||||
# Or manually initialize
|
||||
flask db init
|
||||
flask db migrate -m "Initial schema"
|
||||
flask db upgrade
|
||||
```
|
||||
|
||||
#### **Comprehensive Migration for Any Existing Database:**
|
||||
```bash
|
||||
# For ANY existing database (recommended)
|
||||
python migrations/migrate_existing_database.py
|
||||
|
||||
# For legacy schema migration
|
||||
python migrations/legacy_schema_migration.py
|
||||
```
|
||||
|
||||
#### **Migration Support:**
|
||||
- ✅ **Fresh Installation**: No existing database
|
||||
- ✅ **Legacy Databases**: Old custom migration systems
|
||||
- ✅ **Mixed Schema**: Some tables exist, some missing
|
||||
- ✅ **Production Data**: Existing databases with user data
|
||||
- ✅ **Cross-Version**: Databases from different TimeTracker versions
|
||||
|
||||
#### **🚀 Automatic Container Migration:**
|
||||
- ✅ **Zero Configuration**: Container automatically detects database state
|
||||
- ✅ **Smart Strategy Selection**: Chooses best migration approach
|
||||
- ✅ **Automatic Startup**: Handles migration during container startup
|
||||
- ✅ **Production Ready**: Safe migration with automatic fallbacks
|
||||
|
||||
See [Migration Documentation](migrations/README.md), [Complete Migration Guide](migrations/MIGRATION_GUIDE.md), and [Container Startup Configuration](docker/STARTUP_MIGRATION_CONFIG.md) for comprehensive details.
|
||||
|
||||
### Enhanced Database Startup
|
||||
|
||||
The application now includes an enhanced database startup procedure that automatically:
|
||||
|
||||
235
app/utils/cli.py
@@ -79,192 +79,53 @@ def register_cli_commands(app):
|
||||
click.echo("For PostgreSQL, please use pg_dump, e.g.: pg_dump --format=custom --dbname=\"$DATABASE_URL\" --file=backup.dump")
|
||||
|
||||
# Clean up old backups
|
||||
backup_dir = os.getenv('BACKUP_DIR', '/data/backups')
|
||||
if os.path.exists(backup_dir):
|
||||
cleanup_old_backups(backup_dir)
|
||||
|
||||
@app.cli.command()
|
||||
@with_appcontext
|
||||
def cleanup_old_entries():
|
||||
"""Clean up old time entries (older than specified days)"""
|
||||
days = click.prompt("Delete entries older than (days)", type=int, default=365)
|
||||
|
||||
cutoff_date = datetime.utcnow() - timedelta(days=days)
|
||||
old_entries = TimeEntry.query.filter(
|
||||
TimeEntry.end_time < cutoff_date
|
||||
).all()
|
||||
|
||||
if not old_entries:
|
||||
click.echo("No old entries found")
|
||||
return
|
||||
|
||||
count = len(old_entries)
|
||||
if click.confirm(f"Delete {count} old entries?"):
|
||||
for entry in old_entries:
|
||||
db.session.delete(entry)
|
||||
db.session.commit()
|
||||
click.echo(f"Deleted {count} old entries")
|
||||
else:
|
||||
click.echo("Operation cancelled")
|
||||
|
||||
@app.cli.command()
|
||||
@with_appcontext
|
||||
def stats():
|
||||
"""Show database statistics"""
|
||||
total_users = User.query.count()
|
||||
active_users = User.query.filter_by(is_active=True).count()
|
||||
total_projects = Project.query.count()
|
||||
active_projects = Project.query.filter_by(status='active').count()
|
||||
total_entries = TimeEntry.query.count()
|
||||
completed_entries = TimeEntry.query.filter(TimeEntry.end_time.isnot(None)).count()
|
||||
active_timers = TimeEntry.query.filter_by(end_time=None).count()
|
||||
|
||||
click.echo("Database Statistics:")
|
||||
click.echo(f" Users: {total_users} (active: {active_users})")
|
||||
click.echo(f" Projects: {total_projects} (active: {active_projects})")
|
||||
click.echo(f" Time Entries: {total_entries} (completed: {completed_entries}, active: {active_timers})")
|
||||
|
||||
# Calculate total hours
|
||||
total_hours = db.session.query(
|
||||
db.func.sum(TimeEntry.duration_seconds)
|
||||
).filter(
|
||||
TimeEntry.end_time.isnot(None)
|
||||
).scalar() or 0
|
||||
|
||||
total_hours = round(total_hours / 3600, 2)
|
||||
click.echo(f" Total Hours: {total_hours}")
|
||||
|
||||
@app.cli.command()
|
||||
def license_status():
|
||||
"""Show license server client status"""
|
||||
try:
|
||||
from app.utils.license_server import get_license_client
|
||||
client = get_license_client()
|
||||
if client:
|
||||
status = client.get_status()
|
||||
click.echo("License Server Client Status:")
|
||||
click.echo(f" Registered: {status['is_registered']}")
|
||||
click.echo(f" Instance ID: {status['instance_id']}")
|
||||
click.echo(f" Running: {status['is_running']}")
|
||||
click.echo(f" Server Healthy: {status['server_healthy']}")
|
||||
click.echo(f" Offline Data: {status['offline_data_count']}")
|
||||
click.echo(f" App ID: {status['app_identifier']}")
|
||||
click.echo(f" App Version: {status['app_version']}")
|
||||
else:
|
||||
click.echo("License server client not initialized")
|
||||
except Exception as e:
|
||||
click.echo(f"Error getting license status: {e}")
|
||||
|
||||
@app.cli.command()
|
||||
def license_test():
|
||||
"""Test license server communication"""
|
||||
try:
|
||||
from app.utils.license_server import get_license_client, send_usage_event
|
||||
client = get_license_client()
|
||||
if client:
|
||||
click.echo("Testing license server communication...")
|
||||
|
||||
# Test server health
|
||||
if client.check_server_health():
|
||||
click.echo("✓ Server is healthy")
|
||||
else:
|
||||
click.echo("✗ Server is not responding")
|
||||
|
||||
# Test usage event
|
||||
if send_usage_event("test_event", {"test": "data"}):
|
||||
click.echo("✓ Usage event sent successfully")
|
||||
else:
|
||||
click.echo("✗ Failed to send usage event")
|
||||
|
||||
else:
|
||||
click.echo("License server client not initialized")
|
||||
except Exception as e:
|
||||
click.echo(f"Error testing license server: {e}")
|
||||
|
||||
@app.cli.command()
|
||||
def license_restart():
|
||||
"""Restart the license server client"""
|
||||
try:
|
||||
from app.utils.license_server import get_license_client, start_license_client
|
||||
client = get_license_client()
|
||||
if client:
|
||||
click.echo("Restarting license server client...")
|
||||
if start_license_client():
|
||||
click.echo("✓ License server client restarted successfully")
|
||||
else:
|
||||
click.echo("✗ Failed to restart license server client")
|
||||
else:
|
||||
click.echo("License server client not initialized")
|
||||
except Exception as e:
|
||||
click.echo(f"Error restarting license server client: {e}")
|
||||
|
||||
def cleanup_old_backups(backup_dir, retention_days=30):
|
||||
"""Clean up old backup files"""
|
||||
cutoff_date = datetime.now() - timedelta(days=retention_days)
|
||||
|
||||
for filename in os.listdir(backup_dir):
|
||||
file_path = os.path.join(backup_dir, filename)
|
||||
if os.path.isfile(file_path):
|
||||
file_time = datetime.fromtimestamp(os.path.getctime(file_path))
|
||||
if file_time < cutoff_date:
|
||||
os.remove(file_path)
|
||||
click.echo(f"Removed old backup: {filename}")
|
||||
|
||||
def cleanup_old_entries():
|
||||
"""Clean up old time entries (older than specified days)"""
|
||||
try:
|
||||
days = 365 # Default to 1 year
|
||||
cutoff_date = datetime.utcnow() - timedelta(days=days)
|
||||
old_entries = TimeEntry.query.filter(
|
||||
TimeEntry.end_time < cutoff_date
|
||||
).all()
|
||||
|
||||
if not old_entries:
|
||||
click.echo("No old entries found")
|
||||
return
|
||||
|
||||
count = len(old_entries)
|
||||
click.echo(f"Found {count} old entries older than {days} days")
|
||||
|
||||
# For automated cleanup, we'll just log the count
|
||||
# In interactive mode, you could add confirmation here
|
||||
click.echo(f"Would delete {count} old entries (use interactive mode for confirmation)")
|
||||
|
||||
except Exception as e:
|
||||
click.echo(f"Error cleaning up old entries: {e}")
|
||||
|
||||
def create_backup():
|
||||
"""Create a database backup"""
|
||||
try:
|
||||
from app.config import Config
|
||||
|
||||
url = Config.SQLALCHEMY_DATABASE_URI
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
|
||||
if url.startswith('sqlite:///'):
|
||||
# SQLite file copy
|
||||
db_path = url.replace('sqlite:///', '')
|
||||
if not os.path.exists(db_path):
|
||||
click.echo(f"Database file not found: {db_path}")
|
||||
return
|
||||
backup_dir = os.path.join(os.path.dirname(db_path), 'backups')
|
||||
os.makedirs(backup_dir, exist_ok=True)
|
||||
backup_filename = f"timetracker_backup_{timestamp}.db"
|
||||
backup_path = os.path.join(backup_dir, backup_filename)
|
||||
|
||||
shutil.copy2(db_path, backup_path)
|
||||
click.echo(f"Database backed up to: {backup_path}")
|
||||
else:
|
||||
click.echo("For PostgreSQL, please use pg_dump, e.g.: pg_dump --format=custom --dbname=\"$DATABASE_URL\" --file=backup.dump")
|
||||
|
||||
except Exception as e:
|
||||
click.echo(f"Error creating backup: {e}")
|
||||
try:
|
||||
backup_retention_days = int(os.getenv('BACKUP_RETENTION_DAYS', 30))
|
||||
cutoff_date = datetime.now() - timedelta(days=backup_retention_days)
|
||||
|
||||
for backup_file in os.listdir(backup_dir):
|
||||
backup_file_path = os.path.join(backup_dir, backup_file)
|
||||
if os.path.isfile(backup_file_path):
|
||||
file_time = datetime.fromtimestamp(os.path.getctime(backup_file_path))
|
||||
if file_time < cutoff_date:
|
||||
os.remove(backup_file_path)
|
||||
click.echo(f"Removed old backup: {backup_file}")
|
||||
except Exception as e:
|
||||
click.echo(f"Warning: Could not clean up old backups: {e}")
|
||||
|
||||
def restore_backup():
|
||||
"""Restore database from backup"""
|
||||
try:
|
||||
click.echo("Database restore functionality not implemented yet.")
|
||||
click.echo("Please restore manually using your database management tools.")
|
||||
|
||||
except Exception as e:
|
||||
click.echo(f"Error restoring backup: {e}")
|
||||
@app.cli.command()
|
||||
@with_appcontext
|
||||
def migrate_to_flask_migrate():
|
||||
"""Migrate from custom migration system to Flask-Migrate"""
|
||||
click.echo("This command is deprecated. Use the migration management script instead:")
|
||||
click.echo("python migrations/manage_migrations.py")
|
||||
click.echo("\nOr use Flask-Migrate commands directly:")
|
||||
click.echo("flask db init # Initialize migrations (first time only)")
|
||||
click.echo("flask db migrate # Create a new migration")
|
||||
click.echo("flask db upgrade # Apply pending migrations")
|
||||
click.echo("flask db downgrade # Rollback last migration")
|
||||
click.echo("flask db current # Show current migration")
|
||||
click.echo("flask db history # Show migration history")
|
||||
|
||||
@app.cli.command()
|
||||
@with_appcontext
|
||||
def db_status():
|
||||
"""Show database migration status"""
|
||||
try:
|
||||
from flask_migrate import current
|
||||
current()
|
||||
except Exception as e:
|
||||
click.echo(f"Error getting migration status: {e}")
|
||||
click.echo("Make sure Flask-Migrate is properly initialized")
|
||||
|
||||
@app.cli.command()
|
||||
@with_appcontext
|
||||
def db_history():
|
||||
"""Show database migration history"""
|
||||
try:
|
||||
from flask_migrate import history
|
||||
history()
|
||||
except Exception as e:
|
||||
click.echo(f"Error getting migration history: {e}")
|
||||
click.echo("Make sure Flask-Migrate is properly initialized")
|
||||
|
||||
BIN
assets/screenshots/Admin.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
assets/screenshots/Clients.png
Normal file
|
After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 77 KiB |
BIN
assets/screenshots/LogTime.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
assets/screenshots/New-Client.png
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
assets/screenshots/New-Project.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
assets/screenshots/New-Task.png
Normal file
|
After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 153 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 76 KiB |
BIN
assets/screenshots/VisualAnalytics.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
281
docker/STARTUP_MIGRATION_CONFIG.md
Normal file
@@ -0,0 +1,281 @@
|
||||
# Container Startup with Automatic Migration Detection
|
||||
|
||||
This document explains how the TimeTracker Docker container automatically detects database state and chooses the correct migration strategy during startup.
|
||||
|
||||
## 🎯 **Automatic Migration Detection**
|
||||
|
||||
The container startup system automatically detects the current state of your database and chooses the appropriate migration strategy:
|
||||
|
||||
### **Database States Detected:**
|
||||
|
||||
| **State** | **Description** | **Detection Method** | **Migration Strategy** |
|
||||
|-----------|----------------|---------------------|------------------------|
|
||||
| **Fresh** | No database or empty database | No tables exist | `fresh_init` |
|
||||
| **Migrated** | Database already uses Flask-Migrate | `alembic_version` table exists | `check_migrations` |
|
||||
| **Legacy** | Database with old custom migrations | Tables exist but no `alembic_version` | `comprehensive_migration` |
|
||||
| **Unknown** | Cannot determine state | Detection failed | `comprehensive_migration` (fallback) |
|
||||
|
||||
## 🚀 **Migration Strategies**
|
||||
|
||||
### **1. Fresh Initialization (`fresh_init`)**
|
||||
- **When Used**: New database, no existing tables
|
||||
- **Actions**:
|
||||
- Initialize Flask-Migrate
|
||||
- Create initial migration
|
||||
- Apply migration to create schema
|
||||
- **Result**: Complete new database with Flask-Migrate
|
||||
|
||||
### **2. Migration Check (`check_migrations`)**
|
||||
- **When Used**: Database already migrated, check for updates
|
||||
- **Actions**:
|
||||
- Check current migration revision
|
||||
- Apply any pending migrations
|
||||
- Verify database integrity
|
||||
- **Result**: Database updated to latest migration
|
||||
|
||||
### **3. Comprehensive Migration (`comprehensive_migration`)**
|
||||
- **When Used**: Legacy database with old custom migrations
|
||||
- **Actions**:
|
||||
- Run enhanced startup script (`startup_with_migration.py`)
|
||||
- Fallback to manual migration if script fails
|
||||
- Preserve all existing data
|
||||
- Create migration baseline
|
||||
- **Result**: Legacy database converted to Flask-Migrate
|
||||
|
||||
## 🔧 **Startup Process Flow**
|
||||
|
||||
```
|
||||
Container Start
|
||||
↓
|
||||
Wait for Database
|
||||
↓
|
||||
Detect Database State
|
||||
↓
|
||||
Choose Migration Strategy
|
||||
↓
|
||||
Execute Migration
|
||||
↓
|
||||
Verify Database Integrity
|
||||
↓
|
||||
Start Application
|
||||
```
|
||||
|
||||
## 📋 **Startup Scripts**
|
||||
|
||||
### **Primary Entrypoint: `docker/entrypoint.sh`**
|
||||
- **Purpose**: Main container entrypoint with migration detection
|
||||
- **Features**:
|
||||
- Database availability check
|
||||
- State detection (PostgreSQL/SQLite)
|
||||
- Strategy selection
|
||||
- Migration execution
|
||||
- Integrity verification
|
||||
- **Fallbacks**: Multiple fallback methods for each step
|
||||
|
||||
### **Enhanced Startup: `docker/startup_with_migration.py`**
|
||||
- **Purpose**: Advanced migration handling for complex scenarios
|
||||
- **Features**:
|
||||
- Comprehensive database analysis
|
||||
- Automatic backup creation
|
||||
- Schema migration
|
||||
- Data preservation
|
||||
- Error recovery
|
||||
|
||||
## 🛡️ **Safety Features**
|
||||
|
||||
### **Automatic Protection:**
|
||||
- ✅ **Database Wait**: Waits for database to be available
|
||||
- ✅ **State Detection**: Analyzes existing database structure
|
||||
- ✅ **Strategy Selection**: Chooses safest migration approach
|
||||
- ✅ **Fallback Methods**: Multiple fallback options for each step
|
||||
- ✅ **Integrity Verification**: Confirms database is working after migration
|
||||
|
||||
### **Error Handling:**
|
||||
- ✅ **Graceful Failures**: Detailed error logging and recovery
|
||||
- ✅ **Retry Logic**: Automatic retries for database connections
|
||||
- ✅ **Fallback Strategies**: Alternative approaches if primary method fails
|
||||
- ✅ **Logging**: Comprehensive logging for troubleshooting
|
||||
|
||||
## 🔍 **Detection Methods**
|
||||
|
||||
### **PostgreSQL Detection:**
|
||||
```bash
|
||||
# Check if alembic_version table exists
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_name = 'alembic_version'
|
||||
);
|
||||
|
||||
# Get list of existing tables
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
ORDER BY table_name;
|
||||
```
|
||||
|
||||
### **SQLite Detection:**
|
||||
```bash
|
||||
# Check if alembic_version table exists
|
||||
SELECT name FROM sqlite_master
|
||||
WHERE type='table' AND name='alembic_version';
|
||||
|
||||
# Get list of existing tables
|
||||
SELECT name FROM sqlite_master WHERE type='table';
|
||||
```
|
||||
|
||||
## 📊 **Migration Strategy Selection Logic**
|
||||
|
||||
```python
|
||||
def choose_migration_strategy(db_state):
|
||||
if db_state == 'fresh':
|
||||
return 'fresh_init' # New database
|
||||
elif db_state == 'migrated':
|
||||
return 'check_migrations' # Already migrated
|
||||
elif db_state == 'legacy':
|
||||
return 'comprehensive_migration' # Old system
|
||||
else:
|
||||
return 'comprehensive_migration' # Fallback
|
||||
```
|
||||
|
||||
## 🚀 **Usage Examples**
|
||||
|
||||
### **Fresh Database:**
|
||||
```bash
|
||||
# Container will automatically:
|
||||
# 1. Detect no tables exist
|
||||
# 2. Choose 'fresh_init' strategy
|
||||
# 3. Initialize Flask-Migrate
|
||||
# 4. Create and apply initial migration
|
||||
# 5. Start application
|
||||
```
|
||||
|
||||
### **Existing Migrated Database:**
|
||||
```bash
|
||||
# Container will automatically:
|
||||
# 1. Detect alembic_version table exists
|
||||
# 2. Choose 'check_migrations' strategy
|
||||
# 3. Check for pending migrations
|
||||
# 4. Apply any updates
|
||||
# 5. Start application
|
||||
```
|
||||
|
||||
### **Legacy Database:**
|
||||
```bash
|
||||
# Container will automatically:
|
||||
# 1. Detect tables exist but no alembic_version
|
||||
# 2. Choose 'comprehensive_migration' strategy
|
||||
# 3. Run enhanced migration script
|
||||
# 4. Preserve all existing data
|
||||
# 5. Convert to Flask-Migrate
|
||||
# 6. Start application
|
||||
```
|
||||
|
||||
## 🔧 **Configuration Options**
|
||||
|
||||
### **Environment Variables:**
|
||||
```bash
|
||||
# Required
|
||||
DATABASE_URL=postgresql://user:pass@host:port/db
|
||||
# or
|
||||
DATABASE_URL=sqlite:///path/to/database.db
|
||||
|
||||
# Optional
|
||||
FLASK_APP=/app/app.py
|
||||
TZ=Europe/Rome
|
||||
```
|
||||
|
||||
### **Database Connection Settings:**
|
||||
- **PostgreSQL**: Full connection string with credentials
|
||||
- **SQLite**: File path with write permissions
|
||||
- **Connection Retries**: 30 attempts with 2-second delays
|
||||
- **Timeout**: 60 seconds total wait time
|
||||
|
||||
## 📝 **Logging and Monitoring**
|
||||
|
||||
### **Log Locations:**
|
||||
- **Container Logs**: `docker logs <container_name>`
|
||||
- **Startup Logs**: `/var/log/timetracker_startup.log`
|
||||
- **Application Logs**: `/app/logs/`
|
||||
|
||||
### **Log Levels:**
|
||||
- **INFO**: Normal operation and progress
|
||||
- **WARNING**: Non-critical issues
|
||||
- **ERROR**: Critical failures and errors
|
||||
|
||||
### **Key Log Messages:**
|
||||
```
|
||||
[2025-01-15 10:00:00] === TimeTracker Docker Container Starting ===
|
||||
[2025-01-15 10:00:01] Waiting for database to be available...
|
||||
[2025-01-15 10:00:02] ✓ PostgreSQL database is available
|
||||
[2025-01-15 10:00:03] Analyzing database state...
|
||||
[2025-01-15 10:00:04] Detected database state: legacy with 8 tables
|
||||
[2025-01-15 10:00:05] Selected migration strategy: comprehensive_migration
|
||||
[2025-01-15 10:00:06] Executing migration strategy: comprehensive_migration
|
||||
[2025-01-15 10:00:10] ✓ Enhanced startup script completed successfully
|
||||
[2025-01-15 10:00:11] Verifying database integrity...
|
||||
[2025-01-15 10:00:12] ✓ Database integrity verified
|
||||
[2025-01-15 10:00:13] === Startup and Migration Complete ===
|
||||
[2025-01-15 10:00:14] Starting TimeTracker application...
|
||||
```
|
||||
|
||||
## 🔍 **Troubleshooting**
|
||||
|
||||
### **Common Issues:**
|
||||
|
||||
#### **1. Database Connection Failed:**
|
||||
```bash
|
||||
# Check environment variables
|
||||
echo $DATABASE_URL
|
||||
|
||||
# Check database service
|
||||
docker-compose ps db
|
||||
|
||||
# Check logs
|
||||
docker logs <container_name>
|
||||
```
|
||||
|
||||
#### **2. Migration Strategy Failed:**
|
||||
```bash
|
||||
# Check migration logs
|
||||
docker exec <container_name> cat /var/log/timetracker_startup.log
|
||||
|
||||
# Check migration status
|
||||
docker exec <container_name> flask db current
|
||||
|
||||
# Check database state manually
|
||||
docker exec <container_name> flask db history
|
||||
```
|
||||
|
||||
#### **3. Database Integrity Check Failed:**
|
||||
```bash
|
||||
# Check if key tables exist
|
||||
docker exec <container_name> psql $DATABASE_URL -c "\dt"
|
||||
|
||||
# Check table structure
|
||||
docker exec <container_name> psql $DATABASE_URL -c "\d users"
|
||||
```
|
||||
|
||||
### **Recovery Options:**
|
||||
1. **Check Logs**: Review startup and migration logs
|
||||
2. **Verify Database**: Ensure database is accessible
|
||||
3. **Check Permissions**: Verify database user permissions
|
||||
4. **Restart Container**: Restart with fresh migration attempt
|
||||
5. **Manual Migration**: Use migration scripts manually if needed
|
||||
|
||||
## 🎉 **Benefits**
|
||||
|
||||
### **Automatic Operation:**
|
||||
- ✅ **Zero Configuration**: Works with any existing database
|
||||
- ✅ **Smart Detection**: Automatically chooses best migration approach
|
||||
- ✅ **Data Preservation**: Never loses existing data
|
||||
- ✅ **Error Recovery**: Multiple fallback methods
|
||||
|
||||
### **Production Ready:**
|
||||
- ✅ **Safe Migration**: Automatic backups and verification
|
||||
- ✅ **Rollback Support**: Can revert to previous state
|
||||
- ✅ **Monitoring**: Comprehensive logging and health checks
|
||||
- ✅ **Scalability**: Works with any database size
|
||||
|
||||
---
|
||||
|
||||
**Result**: Your TimeTracker container will automatically handle any database scenario during startup, ensuring a smooth transition to the new Flask-Migrate system regardless of the existing database state! 🚀
|
||||
286
docker/TROUBLESHOOTING_DB_CONNECTION.md
Normal file
@@ -0,0 +1,286 @@
|
||||
# Database Connection Troubleshooting Guide
|
||||
|
||||
This guide helps resolve database connection issues during TimeTracker container startup.
|
||||
|
||||
## 🚨 **Common Error: Database Connection Failed**
|
||||
|
||||
### **Error Symptoms:**
|
||||
```
|
||||
[2025-09-01 19:02:16] Database not ready (attempt 1/30)
|
||||
[2025-09-01 19:02:18] Database not ready (attempt 2/30)
|
||||
...
|
||||
[2025-09-01 19:02:46] Database not ready (attempt 17/30)
|
||||
```
|
||||
|
||||
### **Root Causes:**
|
||||
1. **PostgreSQL service not fully initialized**
|
||||
2. **Database container not ready**
|
||||
3. **Network connectivity issues**
|
||||
4. **Authentication problems**
|
||||
5. **Connection string format issues**
|
||||
|
||||
## 🔧 **Immediate Solutions**
|
||||
|
||||
### **1. Check Database Service Status**
|
||||
```bash
|
||||
# Check if database container is running
|
||||
docker-compose ps db
|
||||
|
||||
# Check database container logs
|
||||
docker-compose logs db
|
||||
|
||||
# Check if database is accepting connections
|
||||
docker-compose exec db pg_isready -U timetracker
|
||||
```
|
||||
|
||||
### **2. Test Database Connection Manually**
|
||||
```bash
|
||||
# Test connection from host
|
||||
docker-compose exec app python /app/docker/test_db_connection.py
|
||||
|
||||
# Or test from outside container
|
||||
docker exec <container_name> python /app/docker/test_db_connection.py
|
||||
```
|
||||
|
||||
### **3. Check Environment Variables**
|
||||
```bash
|
||||
# Verify DATABASE_URL is set correctly
|
||||
docker-compose exec app env | grep DATABASE_URL
|
||||
|
||||
# Check if the URL format is correct
|
||||
echo $DATABASE_URL
|
||||
```
|
||||
|
||||
## 🔍 **Diagnostic Steps**
|
||||
|
||||
### **Step 1: Verify Database Container**
|
||||
```bash
|
||||
# Check if PostgreSQL container is healthy
|
||||
docker-compose ps
|
||||
|
||||
# Look for these indicators:
|
||||
# - Status: Up (healthy)
|
||||
# - Health: healthy
|
||||
```
|
||||
|
||||
### **Step 2: Check Database Logs**
|
||||
```bash
|
||||
# View PostgreSQL startup logs
|
||||
docker-compose logs db | tail -50
|
||||
|
||||
# Look for these success indicators:
|
||||
# - "database system is ready to accept connections"
|
||||
# - "PostgreSQL init process complete"
|
||||
# - "database system is ready to accept connections"
|
||||
```
|
||||
|
||||
### **Step 3: Test Network Connectivity**
|
||||
```bash
|
||||
# Test if app container can reach database
|
||||
docker-compose exec app ping db
|
||||
|
||||
# Test if database port is accessible
|
||||
docker-compose exec app nc -zv db 5432
|
||||
```
|
||||
|
||||
### **Step 4: Verify Database Credentials**
|
||||
```bash
|
||||
# Check if database user exists
|
||||
docker-compose exec db psql -U postgres -c "\du"
|
||||
|
||||
# Verify database exists
|
||||
docker-compose exec db psql -U postgres -c "\l"
|
||||
```
|
||||
|
||||
## 🛠️ **Common Fixes**
|
||||
|
||||
### **Fix 1: Wait for Database to be Ready**
|
||||
```bash
|
||||
# Stop all services
|
||||
docker-compose down
|
||||
|
||||
# Start database first and wait
|
||||
docker-compose up -d db
|
||||
|
||||
# Wait for database to be healthy
|
||||
docker-compose ps db
|
||||
|
||||
# Then start app
|
||||
docker-compose up -d app
|
||||
```
|
||||
|
||||
### **Fix 2: Check Connection String Format**
|
||||
```bash
|
||||
# Correct format for PostgreSQL
|
||||
DATABASE_URL=postgresql://user:password@host:port/database
|
||||
|
||||
# If using psycopg2 (automatic)
|
||||
DATABASE_URL=postgresql+psycopg2://user:password@host:port/database
|
||||
|
||||
# Common issues:
|
||||
# - Missing password
|
||||
# - Wrong port number
|
||||
# - Database name doesn't exist
|
||||
```
|
||||
|
||||
### **Fix 3: Verify Database Initialization**
|
||||
```bash
|
||||
# Check if database was initialized
|
||||
docker-compose exec db psql -U timetracker -d timetracker -c "\dt"
|
||||
|
||||
# If no tables exist, database might not be initialized
|
||||
# Check docker/init-database.py or similar scripts
|
||||
```
|
||||
|
||||
### **Fix 4: Check Docker Compose Configuration**
|
||||
```yaml
|
||||
# Ensure proper service dependencies
|
||||
services:
|
||||
app:
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
# ... other config
|
||||
|
||||
db:
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U timetracker"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
```
|
||||
|
||||
## 📋 **Troubleshooting Checklist**
|
||||
|
||||
### **Before Starting Container:**
|
||||
- [ ] Database container is running and healthy
|
||||
- [ ] DATABASE_URL environment variable is set correctly
|
||||
- [ ] Database user has proper permissions
|
||||
- [ ] Database exists and is accessible
|
||||
- [ ] Network connectivity between containers works
|
||||
|
||||
### **During Startup:**
|
||||
- [ ] Container waits for database to be ready
|
||||
- [ ] Connection string is parsed correctly
|
||||
- [ ] Authentication succeeds
|
||||
- [ ] Basic queries can be executed
|
||||
- [ ] Migration system can access database
|
||||
|
||||
### **After Startup:**
|
||||
- [ ] Database tables are accessible
|
||||
- [ ] Migration system works correctly
|
||||
- [ ] Application can read/write data
|
||||
- [ ] Health checks pass
|
||||
|
||||
## 🔧 **Advanced Debugging**
|
||||
|
||||
### **Enable Verbose Logging**
|
||||
```bash
|
||||
# Set environment variable for verbose logging
|
||||
export FLASK_DEBUG=1
|
||||
export PYTHONVERBOSE=1
|
||||
|
||||
# Restart container with verbose logging
|
||||
docker-compose up -d app
|
||||
```
|
||||
|
||||
### **Test Connection Step by Step**
|
||||
```bash
|
||||
# 1. Test basic connectivity
|
||||
docker-compose exec app ping db
|
||||
|
||||
# 2. Test port accessibility
|
||||
docker-compose exec app nc -zv db 5432
|
||||
|
||||
# 3. Test PostgreSQL connection
|
||||
docker-compose exec app python -c "
|
||||
import psycopg2
|
||||
try:
|
||||
conn = psycopg2.connect('postgresql://timetracker:timetracker@db:5432/timetracker')
|
||||
print('Connection successful')
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
print(f'Connection failed: {e}')
|
||||
"
|
||||
|
||||
# 4. Test with psycopg2 URL
|
||||
docker-compose exec app python -c "
|
||||
import psycopg2
|
||||
try:
|
||||
conn = psycopg2.connect('postgresql+psycopg2://timetracker:timetracker@db:5432/timetracker')
|
||||
print('Connection successful')
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
print(f'Connection failed: {e}')
|
||||
"
|
||||
```
|
||||
|
||||
### **Check Container Resources**
|
||||
```bash
|
||||
# Check if containers have enough resources
|
||||
docker stats
|
||||
|
||||
# Check container logs for resource issues
|
||||
docker-compose logs app | grep -i "memory\|cpu\|disk"
|
||||
```
|
||||
|
||||
## 🚀 **Prevention Strategies**
|
||||
|
||||
### **1. Use Health Checks**
|
||||
```yaml
|
||||
# In docker-compose.yml
|
||||
services:
|
||||
db:
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U timetracker"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
app:
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
```
|
||||
|
||||
### **2. Proper Service Dependencies**
|
||||
```yaml
|
||||
# Ensure app waits for database
|
||||
services:
|
||||
app:
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
### **3. Connection Retry Logic**
|
||||
```bash
|
||||
# The entrypoint script already includes:
|
||||
# - 60 retry attempts
|
||||
# - 3-second delays
|
||||
# - Multiple connection methods
|
||||
# - Fallback strategies
|
||||
```
|
||||
|
||||
## 📞 **Getting Help**
|
||||
|
||||
### **If Problems Persist:**
|
||||
1. **Check all logs**: `docker-compose logs`
|
||||
2. **Verify environment**: `docker-compose config`
|
||||
3. **Test manually**: Use the test script
|
||||
4. **Check documentation**: See `docker/STARTUP_MIGRATION_CONFIG.md`
|
||||
|
||||
### **Useful Commands:**
|
||||
```bash
|
||||
# Comprehensive debugging
|
||||
docker-compose logs -f
|
||||
docker-compose exec app python /app/docker/test_db_connection.py
|
||||
docker-compose exec db pg_isready -U timetracker
|
||||
docker-compose ps
|
||||
docker network ls
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Remember**: Most database connection issues are resolved by ensuring the PostgreSQL service is fully initialized before the application container tries to connect. The enhanced entrypoint script includes multiple fallback methods and increased retry logic to handle this automatically.
|
||||
286
docker/debug_startup.sh
Normal file
@@ -0,0 +1,286 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# TimeTracker Startup Debug Script
|
||||
# This script helps debug startup issues step by step
|
||||
|
||||
echo "=== TimeTracker Startup Debug Script ==="
|
||||
echo "Timestamp: $(date)"
|
||||
echo "Container ID: $(hostname)"
|
||||
echo
|
||||
|
||||
# Function to log messages with timestamp
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
|
||||
}
|
||||
|
||||
# Function to check if a command exists
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Function to test basic connectivity
|
||||
test_basic_connectivity() {
|
||||
log "=== Testing Basic Connectivity ==="
|
||||
|
||||
# Test if we can resolve the database hostname
|
||||
if ping -c 1 db >/dev/null 2>&1; then
|
||||
log "✓ Can ping database host 'db'"
|
||||
else
|
||||
log "✗ Cannot ping database host 'db'"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Test if database port is accessible
|
||||
if command_exists nc; then
|
||||
if nc -zv db 5432 2>/dev/null; then
|
||||
log "✓ Database port 5432 is accessible"
|
||||
else
|
||||
log "✗ Database port 5432 is not accessible"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
log "⚠ netcat not available, skipping port test"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Function to test environment variables
|
||||
test_environment() {
|
||||
log "=== Testing Environment Variables ==="
|
||||
|
||||
# Check if DATABASE_URL is set
|
||||
if [[ -n "${DATABASE_URL}" ]]; then
|
||||
log "✓ DATABASE_URL is set: ${DATABASE_URL}"
|
||||
|
||||
# Check if it's a valid PostgreSQL URL
|
||||
if [[ "${DATABASE_URL}" == postgresql* ]]; then
|
||||
log "✓ DATABASE_URL format is valid PostgreSQL"
|
||||
else
|
||||
log "✗ DATABASE_URL format is not valid PostgreSQL"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
log "✗ DATABASE_URL is not set"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check other important variables
|
||||
if [[ -n "${FLASK_APP}" ]]; then
|
||||
log "✓ FLASK_APP is set: ${FLASK_APP}"
|
||||
else
|
||||
log "⚠ FLASK_APP is not set"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Function to test Python dependencies
|
||||
test_python_dependencies() {
|
||||
log "=== Testing Python Dependencies ==="
|
||||
|
||||
# Check if psycopg2 is available
|
||||
if python -c "import psycopg2; print('✓ psycopg2 is available')" 2>/dev/null; then
|
||||
log "✓ psycopg2 is available"
|
||||
else
|
||||
log "✗ psycopg2 is not available"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check if Flask is available
|
||||
if python -c "import flask; print('✓ Flask is available')" 2>/dev/null; then
|
||||
log "✓ Flask is available"
|
||||
else
|
||||
log "✗ Flask is not available"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check if Flask-Migrate is available
|
||||
if python -c "import flask_migrate; print('✓ Flask-Migrate is available')" 2>/dev/null; then
|
||||
log "✓ Flask-Migrate is available"
|
||||
else
|
||||
log "✗ Flask-Migrate is not available"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Function to test database connection
|
||||
test_database_connection() {
|
||||
log "=== Testing Database Connection ==="
|
||||
|
||||
# Run the connection test script
|
||||
if [[ -f "/app/docker/test_db_connection.py" ]]; then
|
||||
log "Running database connection test..."
|
||||
if python /app/docker/test_db_connection.py; then
|
||||
log "✓ Database connection test successful"
|
||||
return 0
|
||||
else
|
||||
log "✗ Database connection test failed"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
log "⚠ Database connection test script not found"
|
||||
|
||||
# Fallback: test connection manually
|
||||
log "Testing connection manually..."
|
||||
if python -c "
|
||||
import psycopg2
|
||||
import sys
|
||||
try:
|
||||
# Parse connection string to remove +psycopg2 if present
|
||||
conn_str = '${DATABASE_URL}'.replace('+psycopg2://', 'postgresql://')
|
||||
print(f'Trying to connect to: {conn_str}')
|
||||
conn = psycopg2.connect(conn_str)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT 1')
|
||||
result = cursor.fetchone()
|
||||
print(f'✓ Connection successful, test query result: {result}')
|
||||
conn.close()
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
print(f'✗ Connection failed: {e}')
|
||||
sys.exit(1)
|
||||
"; then
|
||||
log "✓ Manual connection test successful"
|
||||
return 0
|
||||
else
|
||||
log "✗ Manual connection test failed"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to test Flask-Migrate commands
|
||||
test_flask_migrate() {
|
||||
log "=== Testing Flask-Migrate Commands ==="
|
||||
|
||||
# Check if flask db command is available
|
||||
if flask db --help >/dev/null 2>&1; then
|
||||
log "✓ Flask-Migrate commands are available"
|
||||
|
||||
# Test current command
|
||||
if flask db current >/dev/null 2>&1; then
|
||||
current_revision=$(flask db current 2>/dev/null | tr -d '\n' || echo "unknown")
|
||||
log "✓ Current migration revision: $current_revision"
|
||||
else
|
||||
log "⚠ Could not get current migration revision"
|
||||
fi
|
||||
else
|
||||
log "✗ Flask-Migrate commands are not available"
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Function to show system information
|
||||
show_system_info() {
|
||||
log "=== System Information ==="
|
||||
|
||||
log "Python version: $(python --version)"
|
||||
log "Flask version: $(flask --version 2>/dev/null || echo 'Flask CLI not available')"
|
||||
log "Working directory: $(pwd)"
|
||||
log "Current user: $(whoami)"
|
||||
log "Environment: $(env | grep -E '(FLASK|DATABASE|PYTHON)' | sort)"
|
||||
|
||||
# Check if we're in a container
|
||||
if [[ -f /.dockerenv ]]; then
|
||||
log "✓ Running in Docker container"
|
||||
else
|
||||
log "⚠ Not running in Docker container"
|
||||
fi
|
||||
|
||||
# Check if we can access the app directory
|
||||
if [[ -d "/app" ]]; then
|
||||
log "✓ /app directory is accessible"
|
||||
log " Contents: $(ls -la /app | head -5)"
|
||||
else
|
||||
log "✗ /app directory is not accessible"
|
||||
fi
|
||||
}
|
||||
|
||||
# Main execution
|
||||
main() {
|
||||
log "Starting TimeTracker startup debug..."
|
||||
|
||||
# Show system information
|
||||
show_system_info
|
||||
echo
|
||||
|
||||
# Test basic connectivity
|
||||
if ! test_basic_connectivity; then
|
||||
log "❌ Basic connectivity test failed"
|
||||
echo
|
||||
log "Troubleshooting connectivity issues:"
|
||||
log "1. Check if database container is running: docker-compose ps db"
|
||||
log "2. Check database logs: docker-compose logs db"
|
||||
log "3. Check network: docker network ls"
|
||||
echo
|
||||
return 1
|
||||
fi
|
||||
echo
|
||||
|
||||
# Test environment variables
|
||||
if ! test_environment; then
|
||||
log "❌ Environment test failed"
|
||||
echo
|
||||
log "Troubleshooting environment issues:"
|
||||
log "1. Check .env file exists and has correct values"
|
||||
log "2. Verify DATABASE_URL format"
|
||||
log "3. Check docker-compose environment section"
|
||||
echo
|
||||
return 1
|
||||
fi
|
||||
echo
|
||||
|
||||
# Test Python dependencies
|
||||
if ! test_python_dependencies; then
|
||||
log "❌ Python dependencies test failed"
|
||||
echo
|
||||
log "Troubleshooting dependency issues:"
|
||||
log "1. Check requirements.txt is installed"
|
||||
log "2. Verify Python packages are available"
|
||||
log "3. Check container build process"
|
||||
echo
|
||||
return 1
|
||||
fi
|
||||
echo
|
||||
|
||||
# Test database connection
|
||||
if ! test_database_connection; then
|
||||
log "❌ Database connection test failed"
|
||||
echo
|
||||
log "Troubleshooting connection issues:"
|
||||
log "1. Check database container health: docker-compose ps db"
|
||||
log "2. Verify database credentials"
|
||||
log "3. Check database initialization"
|
||||
log "4. See: docker/TROUBLESHOOTING_DB_CONNECTION.md"
|
||||
echo
|
||||
return 1
|
||||
fi
|
||||
echo
|
||||
|
||||
# Test Flask-Migrate
|
||||
if ! test_flask_migrate; then
|
||||
log "❌ Flask-Migrate test failed"
|
||||
echo
|
||||
log "Troubleshooting Flask-Migrate issues:"
|
||||
log "1. Check if migrations directory exists"
|
||||
log "2. Verify Flask-Migrate is properly installed"
|
||||
log "3. Check application configuration"
|
||||
echo
|
||||
return 1
|
||||
fi
|
||||
echo
|
||||
|
||||
log "🎉 All tests passed! System appears to be ready."
|
||||
log "You can now try starting the application normally."
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main "$@"
|
||||
479
docker/entrypoint.sh
Normal file
@@ -0,0 +1,479 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# TimeTracker Docker Entrypoint with Automatic Migration Detection
|
||||
# This script automatically detects database state and chooses the correct migration strategy
|
||||
|
||||
echo "=== TimeTracker Docker Container Starting ==="
|
||||
echo "Timestamp: $(date)"
|
||||
echo "Container ID: $(hostname)"
|
||||
echo "Python version: $(python --version)"
|
||||
echo "Flask version: $(flask --version 2>/dev/null || echo 'Flask CLI not available')"
|
||||
|
||||
# Function to log messages with timestamp
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
|
||||
}
|
||||
|
||||
# Function to check if a command exists
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Function to wait for database
|
||||
wait_for_database() {
|
||||
local db_url="$1"
|
||||
local max_retries=60 # Increased retries
|
||||
local retry_delay=3 # Increased delay
|
||||
|
||||
log "Waiting for database to be available..."
|
||||
|
||||
for attempt in $(seq 1 $max_retries); do
|
||||
if [[ "$db_url" == postgresql://* ]]; then
|
||||
# PostgreSQL connection test
|
||||
if command_exists psql; then
|
||||
if psql "$db_url" -c "SELECT 1" >/dev/null 2>&1; then
|
||||
log "✓ PostgreSQL database is available (via psql)"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Always try Python connection as primary method
|
||||
if python -c "
|
||||
import psycopg2
|
||||
import sys
|
||||
try:
|
||||
# Parse connection string to remove +psycopg2 if present
|
||||
conn_str = '$db_url'.replace('+psycopg2://', 'postgresql://')
|
||||
conn = psycopg2.connect(conn_str)
|
||||
conn.close()
|
||||
print('Connection successful')
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
print(f'Connection failed: {e}')
|
||||
sys.exit(1)
|
||||
" >/dev/null 2>&1; then
|
||||
log "✓ PostgreSQL database is available (via Python)"
|
||||
return 0
|
||||
fi
|
||||
elif [[ "$db_url" == sqlite://* ]]; then
|
||||
# SQLite file check
|
||||
local db_file="${db_url#sqlite://}"
|
||||
if [[ -f "$db_file" ]] || [[ -w "$(dirname "$db_file")" ]]; then
|
||||
log "✓ SQLite database is available"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
log "Database not ready (attempt $attempt/$max_retries)"
|
||||
if [[ $attempt -lt $max_retries ]]; then
|
||||
sleep $retry_delay
|
||||
fi
|
||||
done
|
||||
|
||||
log "✗ Database is not available after maximum retries"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Function to detect database state
|
||||
detect_database_state() {
|
||||
local db_url="$1"
|
||||
log "Analyzing database state..."
|
||||
|
||||
if [[ "$db_url" == postgresql://* ]]; then
|
||||
# PostgreSQL state detection
|
||||
if command_exists psql; then
|
||||
# Check if alembic_version table exists
|
||||
local has_alembic=$(psql "$db_url" -t -c "
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_name = 'alembic_version'
|
||||
);
|
||||
" 2>/dev/null | tr -d ' ')
|
||||
|
||||
# Get list of existing tables
|
||||
local existing_tables=$(psql "$db_url" -t -c "
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
ORDER BY table_name;
|
||||
" 2>/dev/null | grep -v '^$' | tr '\n' ' ')
|
||||
|
||||
log "PostgreSQL state: has_alembic=$has_alembic, tables=[$existing_tables]"
|
||||
|
||||
if [[ "$has_alembic" == "t" ]]; then
|
||||
echo "migrated"
|
||||
elif [[ -n "$existing_tables" ]]; then
|
||||
echo "legacy"
|
||||
else
|
||||
echo "fresh"
|
||||
fi
|
||||
else
|
||||
# Fallback to Python detection
|
||||
local state=$(python -c "
|
||||
import psycopg2
|
||||
try:
|
||||
conn = psycopg2.connect('$db_url')
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check if alembic_version table exists
|
||||
cursor.execute(\"\"\"
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_name = 'alembic_version'
|
||||
)
|
||||
\"\"\")
|
||||
has_alembic = cursor.fetchone()[0]
|
||||
|
||||
# Get list of existing tables
|
||||
cursor.execute(\"\"\"
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
ORDER BY table_name
|
||||
\"\"\")
|
||||
existing_tables = [row[0] for row in cursor.fetchall()]
|
||||
|
||||
conn.close()
|
||||
|
||||
if has_alembic:
|
||||
print('migrated')
|
||||
elif existing_tables:
|
||||
print('legacy')
|
||||
else:
|
||||
print('fresh')
|
||||
except Exception as e:
|
||||
print('unknown')
|
||||
" 2>/dev/null)
|
||||
echo "$state"
|
||||
fi
|
||||
elif [[ "$db_url" == sqlite://* ]]; then
|
||||
# SQLite state detection
|
||||
local db_file="${db_url#sqlite://}"
|
||||
|
||||
if [[ ! -f "$db_file" ]]; then
|
||||
echo "fresh"
|
||||
return
|
||||
fi
|
||||
|
||||
local state=$(python -c "
|
||||
import sqlite3
|
||||
try:
|
||||
conn = sqlite3.connect('$db_file')
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check if alembic_version table exists
|
||||
cursor.execute('SELECT name FROM sqlite_master WHERE type=\"table\" AND name=\"alembic_version\"')
|
||||
has_alembic = cursor.fetchone() is not None
|
||||
|
||||
# Get list of existing tables
|
||||
cursor.execute('SELECT name FROM sqlite_master WHERE type=\"table\"')
|
||||
existing_tables = [row[0] for row in cursor.fetchall()]
|
||||
|
||||
conn.close()
|
||||
|
||||
if has_alembic:
|
||||
print('migrated')
|
||||
elif existing_tables:
|
||||
print('legacy')
|
||||
else:
|
||||
print('fresh')
|
||||
except Exception as e:
|
||||
print('unknown')
|
||||
" 2>/dev/null)
|
||||
echo "$state"
|
||||
else
|
||||
echo "unknown"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to choose migration strategy
|
||||
choose_migration_strategy() {
|
||||
local db_state="$1"
|
||||
log "Choosing migration strategy for state: $db_state"
|
||||
|
||||
case "$db_state" in
|
||||
"fresh")
|
||||
log "Fresh database detected - using standard initialization"
|
||||
echo "fresh_init"
|
||||
;;
|
||||
"migrated")
|
||||
log "Database already migrated - checking for pending migrations"
|
||||
echo "check_migrations"
|
||||
;;
|
||||
"legacy")
|
||||
log "Legacy database detected - using comprehensive migration"
|
||||
echo "comprehensive_migration"
|
||||
;;
|
||||
*)
|
||||
log "Unknown database state - using comprehensive migration as fallback"
|
||||
echo "comprehensive_migration"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Function to execute migration strategy
|
||||
execute_migration_strategy() {
|
||||
local strategy="$1"
|
||||
local db_url="$2"
|
||||
|
||||
log "Executing migration strategy: $strategy"
|
||||
|
||||
case "$strategy" in
|
||||
"fresh_init")
|
||||
execute_fresh_init "$db_url"
|
||||
;;
|
||||
"check_migrations")
|
||||
execute_check_migrations "$db_url"
|
||||
;;
|
||||
"comprehensive_migration")
|
||||
execute_comprehensive_migration "$db_url"
|
||||
;;
|
||||
*)
|
||||
log "✗ Unknown migration strategy: $strategy"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Function to execute fresh database initialization
|
||||
execute_fresh_init() {
|
||||
local db_url="$1"
|
||||
log "Executing fresh database initialization..."
|
||||
|
||||
# Initialize Flask-Migrate
|
||||
if ! flask db init >/dev/null 2>&1; then
|
||||
log "✗ Flask-Migrate initialization failed"
|
||||
return 1
|
||||
fi
|
||||
log "✓ Flask-Migrate initialized"
|
||||
|
||||
# Create initial migration
|
||||
if ! flask db migrate -m "Initial database schema" >/dev/null 2>&1; then
|
||||
log "✗ Initial migration creation failed"
|
||||
return 1
|
||||
fi
|
||||
log "✓ Initial migration created"
|
||||
|
||||
# Apply migration
|
||||
if ! flask db upgrade >/dev/null 2>&1; then
|
||||
log "✗ Initial migration application failed"
|
||||
return 1
|
||||
fi
|
||||
log "✓ Initial migration applied"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Function to check and apply pending migrations
|
||||
execute_check_migrations() {
|
||||
local db_url="$1"
|
||||
log "Checking for pending migrations..."
|
||||
|
||||
# Check current migration status
|
||||
local current_revision=$(flask db current 2>/dev/null | tr -d '\n' || echo "unknown")
|
||||
log "Current migration revision: $current_revision"
|
||||
|
||||
# Check for pending migrations
|
||||
if ! flask db upgrade >/dev/null 2>&1; then
|
||||
log "✗ Migration check failed"
|
||||
return 1
|
||||
fi
|
||||
log "✓ Migrations checked and applied"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Function to execute comprehensive migration
|
||||
execute_comprehensive_migration() {
|
||||
local db_url="$1"
|
||||
log "Executing comprehensive migration..."
|
||||
|
||||
# Try to run the enhanced startup script
|
||||
if [[ -f "/app/docker/startup_with_migration.py" ]]; then
|
||||
log "Running enhanced startup script..."
|
||||
if python /app/docker/startup_with_migration.py; then
|
||||
log "✓ Enhanced startup script completed successfully"
|
||||
return 0
|
||||
else
|
||||
log "⚠ Enhanced startup script failed, falling back to manual migration"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Fallback to manual migration
|
||||
log "Executing manual migration fallback..."
|
||||
|
||||
# Initialize Flask-Migrate if not already done
|
||||
if [[ ! -f "/app/migrations/env.py" ]]; then
|
||||
if ! flask db init >/dev/null 2>&1; then
|
||||
log "✗ Flask-Migrate initialization failed"
|
||||
return 1
|
||||
fi
|
||||
log "✓ Flask-Migrate initialized"
|
||||
fi
|
||||
|
||||
# Create baseline migration
|
||||
if ! flask db migrate -m "Baseline from existing database" >/dev/null 2>&1; then
|
||||
log "✗ Baseline migration creation failed"
|
||||
return 1
|
||||
fi
|
||||
log "✓ Baseline migration created"
|
||||
|
||||
# Stamp database as current
|
||||
if ! flask db stamp head >/dev/null 2>&1; then
|
||||
log "✗ Database stamping failed"
|
||||
return 1
|
||||
fi
|
||||
log "✓ Database stamped as current"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Function to verify database integrity
|
||||
verify_database_integrity() {
|
||||
local db_url="$1"
|
||||
log "Verifying database integrity..."
|
||||
|
||||
# Test basic database operations
|
||||
if [[ "$db_url" == postgresql://* ]]; then
|
||||
if command_exists psql; then
|
||||
# Check if key tables exist
|
||||
local key_tables=$(psql "$db_url" -t -c "
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_name IN ('users', 'projects', 'time_entries')
|
||||
AND table_schema = 'public';
|
||||
" 2>/dev/null | grep -v '^$' | tr '\n' ' ')
|
||||
|
||||
if [[ $(echo "$key_tables" | wc -w) -ge 2 ]]; then
|
||||
log "✓ Database integrity verified (PostgreSQL)"
|
||||
return 0
|
||||
else
|
||||
log "✗ Missing key tables: [$key_tables]"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
# Fallback to Python verification
|
||||
if python -c "
|
||||
import psycopg2
|
||||
try:
|
||||
conn = psycopg2.connect('$db_url')
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute(\"\"\"
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_name IN ('users', 'projects', 'time_entries')
|
||||
AND table_schema = 'public'
|
||||
\"\"\")
|
||||
key_tables = [row[0] for row in cursor.fetchall()]
|
||||
|
||||
conn.close()
|
||||
|
||||
if len(key_tables) >= 2:
|
||||
exit(0)
|
||||
else:
|
||||
exit(1)
|
||||
except:
|
||||
exit(1)
|
||||
" >/dev/null 2>&1; then
|
||||
log "✓ Database integrity verified (PostgreSQL via Python)"
|
||||
return 0
|
||||
else
|
||||
log "✗ Database integrity check failed (PostgreSQL)"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
elif [[ "$db_url" == sqlite://* ]]; then
|
||||
local db_file="${db_url#sqlite://}"
|
||||
|
||||
if [[ ! -f "$db_file" ]]; then
|
||||
log "✗ SQLite database file not found"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if python -c "
|
||||
import sqlite3
|
||||
try:
|
||||
conn = sqlite3.connect('$db_file')
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('SELECT name FROM sqlite_master WHERE type=\"table\" AND name IN (\"users\", \"projects\", \"time_entries\")')
|
||||
key_tables = [row[0] for row in cursor.fetchall()]
|
||||
|
||||
conn.close()
|
||||
|
||||
if len(key_tables) >= 2:
|
||||
exit(0)
|
||||
else:
|
||||
exit(1)
|
||||
except:
|
||||
exit(1)
|
||||
" >/dev/null 2>&1; then
|
||||
log "✓ Database integrity verified (SQLite)"
|
||||
return 0
|
||||
else
|
||||
log "✗ Database integrity check failed (SQLite)"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Main execution
|
||||
main() {
|
||||
log "=== TimeTracker Docker Entrypoint with Migration Detection ==="
|
||||
|
||||
# Set environment variables
|
||||
export FLASK_APP=${FLASK_APP:-/app/app.py}
|
||||
|
||||
# Get database URL from environment
|
||||
local db_url="${DATABASE_URL}"
|
||||
if [[ -z "$db_url" ]]; then
|
||||
log "✗ DATABASE_URL environment variable not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "Database URL: $db_url"
|
||||
|
||||
# Wait for database to be available
|
||||
if ! wait_for_database "$db_url"; then
|
||||
log "✗ Failed to connect to database"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Detect database state
|
||||
local db_state=$(detect_database_state "$db_url")
|
||||
log "Detected database state: $db_state"
|
||||
|
||||
# Choose migration strategy
|
||||
local strategy=$(choose_migration_strategy "$db_state")
|
||||
log "Selected migration strategy: $strategy"
|
||||
|
||||
# Execute migration strategy
|
||||
if ! execute_migration_strategy "$strategy" "$db_url"; then
|
||||
log "✗ Migration strategy execution failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify database integrity
|
||||
if ! verify_database_integrity "$db_url"; then
|
||||
log "✗ Database integrity verification failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "=== Startup and Migration Complete ==="
|
||||
log "Database is ready for use"
|
||||
|
||||
# Show final migration status
|
||||
local final_status=$(flask db current 2>/dev/null | tr -d '\n' || echo "unknown")
|
||||
log "Final migration status: $final_status"
|
||||
|
||||
# Start the application
|
||||
log "Starting TimeTracker application..."
|
||||
exec "$@"
|
||||
}
|
||||
|
||||
# Run main function with all arguments
|
||||
main "$@"
|
||||
660
docker/entrypoint_fixed.sh
Normal file
@@ -0,0 +1,660 @@
|
||||
#!/bin/bash
|
||||
# TimeTracker Docker Entrypoint with Automatic Migration Detection
|
||||
# This script automatically detects database state and chooses the correct migration strategy
|
||||
|
||||
# Don't exit on errors - let the script continue and show what's happening
|
||||
# set -e
|
||||
|
||||
echo "=== TimeTracker Docker Container Starting ==="
|
||||
echo "Timestamp: $(date)"
|
||||
echo "Container ID: $(hostname)"
|
||||
echo "Python version: $(python --version 2>/dev/null || echo 'Python not available')"
|
||||
echo "Flask version: $(flask --version 2>/dev/null || echo 'Flask CLI not available')"
|
||||
echo "Current directory: $(pwd)"
|
||||
echo "User: $(whoami)"
|
||||
echo
|
||||
|
||||
# Function to log messages with timestamp
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
|
||||
}
|
||||
|
||||
# Function to check if a command exists
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Function to wait for database
|
||||
wait_for_database() {
|
||||
local db_url="$1"
|
||||
local max_retries=60 # Increased retries
|
||||
local retry_delay=3 # Increased delay
|
||||
|
||||
log "Waiting for database to be available..."
|
||||
log "Database URL: $db_url"
|
||||
|
||||
for attempt in $(seq 1 $max_retries); do
|
||||
log "Attempt $attempt/$max_retries to connect to database..."
|
||||
|
||||
if [[ "$db_url" == postgresql* ]]; then
|
||||
log "Testing PostgreSQL connection..."
|
||||
|
||||
# Test 1: Try psql if available
|
||||
if command_exists psql; then
|
||||
log "Testing with psql..."
|
||||
if psql "$db_url" -c "SELECT 1" >/dev/null 2>&1; then
|
||||
log "✓ PostgreSQL database is available (via psql)"
|
||||
return 0
|
||||
else
|
||||
log "psql connection failed"
|
||||
fi
|
||||
else
|
||||
log "psql not available, skipping psql test"
|
||||
fi
|
||||
|
||||
# Test 2: Always try Python connection
|
||||
log "Testing with Python psycopg2..."
|
||||
if python -c "
|
||||
import psycopg2
|
||||
import sys
|
||||
try:
|
||||
# Parse connection string to remove +psycopg2 if present
|
||||
conn_str = '$db_url'.replace('+psycopg2://', '://')
|
||||
print(f'Attempting connection to: {conn_str}')
|
||||
conn = psycopg2.connect(conn_str)
|
||||
conn.close()
|
||||
print('Connection successful')
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
print(f'Connection failed: {e}')
|
||||
sys.exit(1)
|
||||
" 2>/dev/null; then
|
||||
log "✓ PostgreSQL database is available (via Python)"
|
||||
return 0
|
||||
else
|
||||
log "Python connection failed"
|
||||
fi
|
||||
|
||||
elif [[ "$db_url" == sqlite://* ]]; then
|
||||
log "Testing SQLite connection..."
|
||||
local db_file="${db_url#sqlite://}"
|
||||
if [[ -f "$db_file" ]] || [[ -w "$(dirname "$db_file")" ]]; then
|
||||
log "✓ SQLite database is available"
|
||||
return 0
|
||||
else
|
||||
log "SQLite file not accessible"
|
||||
fi
|
||||
else
|
||||
log "Unknown database URL format: $db_url"
|
||||
fi
|
||||
|
||||
log "Database not ready (attempt $attempt/$max_retries)"
|
||||
if [[ $attempt -lt $max_retries ]]; then
|
||||
log "Waiting $retry_delay seconds before next attempt..."
|
||||
sleep $retry_delay
|
||||
fi
|
||||
done
|
||||
|
||||
log "✗ Database is not available after maximum retries"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Function to detect database state
|
||||
detect_database_state() {
|
||||
local db_url="$1"
|
||||
|
||||
if [[ "$db_url" == postgresql* ]]; then
|
||||
# Simple direct Python execution without temp files
|
||||
python -c "
|
||||
import psycopg2
|
||||
try:
|
||||
conn_str = '$db_url'.replace('+psycopg2://', '://')
|
||||
conn = psycopg2.connect(conn_str)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute(\"\"\"
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_name = 'alembic_version'
|
||||
)
|
||||
\"\"\")
|
||||
has_alembic = cursor.fetchone()[0]
|
||||
|
||||
cursor.execute(\"\"\"
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
ORDER BY table_name
|
||||
\"\"\")
|
||||
existing_tables = [row[0] for row in cursor.fetchall()]
|
||||
|
||||
conn.close()
|
||||
|
||||
if has_alembic:
|
||||
print('migrated')
|
||||
elif existing_tables:
|
||||
print('legacy')
|
||||
else:
|
||||
print('fresh')
|
||||
except Exception as e:
|
||||
print('unknown')
|
||||
" 2>/dev/null
|
||||
|
||||
elif [[ "$db_url" == sqlite://* ]]; then
|
||||
local db_file="${db_url#sqlite://}"
|
||||
|
||||
if [[ ! -f "$db_file" ]]; then
|
||||
echo "fresh"
|
||||
return
|
||||
fi
|
||||
|
||||
python -c "
|
||||
import sqlite3
|
||||
try:
|
||||
conn = sqlite3.connect('$db_file')
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('SELECT name FROM sqlite_master WHERE type=\"table\" AND name=\"alembic_version\"')
|
||||
has_alembic = cursor.fetchone() is not None
|
||||
|
||||
cursor.execute('SELECT name FROM sqlite_master WHERE type=\"table\"')
|
||||
existing_tables = [row[0] for row in cursor.fetchall()]
|
||||
|
||||
conn.close()
|
||||
|
||||
if has_alembic:
|
||||
print('migrated')
|
||||
elif existing_tables:
|
||||
print('legacy')
|
||||
else:
|
||||
print('fresh')
|
||||
except Exception as e:
|
||||
print('unknown')
|
||||
" 2>/dev/null
|
||||
else
|
||||
echo "unknown"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to choose migration strategy
|
||||
choose_migration_strategy() {
|
||||
local db_state="$1"
|
||||
|
||||
case "$db_state" in
|
||||
"fresh")
|
||||
echo "fresh_init"
|
||||
;;
|
||||
"migrated")
|
||||
echo "check_migrations"
|
||||
;;
|
||||
"legacy")
|
||||
echo "comprehensive_migration"
|
||||
;;
|
||||
*)
|
||||
echo "comprehensive_migration"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Function to execute migration strategy
|
||||
execute_migration_strategy() {
|
||||
local strategy="$1"
|
||||
local db_url="$2"
|
||||
|
||||
log "Executing migration strategy: '$strategy'"
|
||||
|
||||
case "$strategy" in
|
||||
"fresh_init")
|
||||
log "Executing fresh_init strategy..."
|
||||
execute_fresh_init "$db_url"
|
||||
;;
|
||||
"check_migrations")
|
||||
log "Executing check_migrations strategy..."
|
||||
execute_check_migrations "$db_url"
|
||||
;;
|
||||
"comprehensive_migration")
|
||||
log "Executing comprehensive_migration strategy..."
|
||||
execute_comprehensive_migration "$db_url"
|
||||
;;
|
||||
*)
|
||||
log "✗ Unknown migration strategy: '$strategy'"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Function to execute fresh database initialization
|
||||
execute_fresh_init() {
|
||||
local db_url="$1"
|
||||
log "Executing fresh database initialization..."
|
||||
|
||||
# Set FLASK_APP if not already set
|
||||
if [[ -z "$FLASK_APP" ]]; then
|
||||
log "⚠ FLASK_APP not set, setting it to app.py"
|
||||
export FLASK_APP=app.py
|
||||
fi
|
||||
|
||||
# Check if migrations directory already exists
|
||||
if [[ -d "/app/migrations" ]]; then
|
||||
log "⚠ Migrations directory already exists, checking if it's properly configured..."
|
||||
|
||||
# Check if we have the required files
|
||||
if [[ -f "/app/migrations/env.py" && -f "/app/migrations/alembic.ini" ]]; then
|
||||
log "✓ Migrations directory is properly configured, skipping init"
|
||||
log "Checking if we need to create initial migration..."
|
||||
|
||||
# Check if we have any migration versions
|
||||
if [[ ! -d "/app/migrations/versions" ]] || [[ -z "$(ls -A /app/migrations/versions 2>/dev/null)" ]]; then
|
||||
log "No migration versions found, creating initial migration..."
|
||||
if ! flask db migrate -m "Initial database schema"; then
|
||||
log "✗ Initial migration creation failed"
|
||||
log "Error details:"
|
||||
flask db migrate -m "Initial database schema" 2>&1 | head -20
|
||||
return 1
|
||||
fi
|
||||
log "✓ Initial migration created"
|
||||
else
|
||||
log "✓ Migration versions already exist"
|
||||
fi
|
||||
|
||||
# Check current migration status
|
||||
log "Checking current migration status..."
|
||||
local current_revision=$(flask db current 2>/dev/null | tr -d '\n' || echo "none")
|
||||
log "Current migration revision: $current_revision"
|
||||
|
||||
if [[ "$current_revision" == "none" ]]; then
|
||||
log "Database not stamped, stamping with current revision..."
|
||||
local head_revision=$(flask db heads 2>/dev/null | tr -d '\n' || echo "")
|
||||
if [[ -n "$head_revision" ]]; then
|
||||
if ! flask db stamp "$head_revision"; then
|
||||
log "✗ Database stamping failed"
|
||||
log "Error details:"
|
||||
flask db stamp "$head_revision" 2>&1 | head -20
|
||||
return 1
|
||||
fi
|
||||
log "✓ Database stamped with revision: $head_revision"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Apply any pending migrations
|
||||
log "Applying pending migrations..."
|
||||
if ! flask db upgrade; then
|
||||
log "✗ Migration application failed"
|
||||
log "Error details:"
|
||||
flask db upgrade 2>&1 | head -20
|
||||
return 1
|
||||
fi
|
||||
log "✓ Migrations applied"
|
||||
|
||||
# Wait a moment for tables to be fully committed
|
||||
log "Waiting for tables to be fully committed..."
|
||||
sleep 2
|
||||
|
||||
return 0
|
||||
else
|
||||
log "⚠ Migrations directory exists but is incomplete, removing it..."
|
||||
rm -rf /app/migrations
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check if we're in the right directory
|
||||
if [[ ! -f "/app/app.py" ]]; then
|
||||
log "⚠ Not in correct directory, changing to /app"
|
||||
cd /app
|
||||
fi
|
||||
|
||||
# Initialize Flask-Migrate
|
||||
log "Initializing Flask-Migrate..."
|
||||
if ! flask db init; then
|
||||
log "✗ Flask-Migrate initialization failed"
|
||||
log "Error details:"
|
||||
flask db init 2>&1 | head -20
|
||||
return 1
|
||||
fi
|
||||
log "✓ Flask-Migrate initialized"
|
||||
|
||||
# Create initial migration
|
||||
log "Creating initial migration..."
|
||||
if ! flask db migrate -m "Initial database schema"; then
|
||||
log "✗ Initial migration creation failed"
|
||||
log "Error details:"
|
||||
flask db migrate -m "Initial database schema" 2>&1 | head -20
|
||||
return 1
|
||||
fi
|
||||
log "✓ Initial migration created"
|
||||
|
||||
# Apply migration
|
||||
log "Applying initial migration..."
|
||||
if ! flask db upgrade; then
|
||||
log "✗ Initial migration application failed"
|
||||
log "Error details:"
|
||||
flask db upgrade 2>&1 | head -20
|
||||
return 1
|
||||
fi
|
||||
log "✓ Initial migration applied"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Function to check and apply pending migrations
|
||||
execute_check_migrations() {
|
||||
local db_url="$1"
|
||||
log "Checking for pending migrations..."
|
||||
|
||||
# Check current migration status
|
||||
local current_revision=$(flask db current 2>/dev/null | tr -d '\n' || echo "unknown")
|
||||
log "Current migration revision: $current_revision"
|
||||
|
||||
# Check for pending migrations
|
||||
if ! flask db upgrade; then
|
||||
log "✗ Migration check failed"
|
||||
log "Error details:"
|
||||
flask db upgrade 2>&1 | head -20
|
||||
return 1
|
||||
fi
|
||||
log "✓ Migrations checked and applied"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Function to execute comprehensive migration
|
||||
execute_comprehensive_migration() {
|
||||
local db_url="$1"
|
||||
log "Executing comprehensive migration..."
|
||||
|
||||
# Try to run the enhanced startup script
|
||||
if [[ -f "/app/docker/startup_with_migration.py" ]]; then
|
||||
log "Running enhanced startup script..."
|
||||
if python /app/docker/startup_with_migration.py; then
|
||||
log "✓ Enhanced startup script completed successfully"
|
||||
return 0
|
||||
else
|
||||
log "⚠ Enhanced startup script failed, falling back to manual migration"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Fallback to manual migration
|
||||
log "Executing manual migration fallback..."
|
||||
|
||||
# Initialize Flask-Migrate if not already done
|
||||
if [[ ! -f "/app/migrations/env.py" ]]; then
|
||||
if ! flask db init; then
|
||||
log "✗ Flask-Migrate initialization failed"
|
||||
log "Error details:"
|
||||
flask db init 2>&1 | head -20
|
||||
return 1
|
||||
fi
|
||||
log "✓ Flask-Migrate initialized"
|
||||
fi
|
||||
|
||||
# Create baseline migration
|
||||
if ! flask db migrate -m "Baseline from existing database"; then
|
||||
log "✗ Baseline migration creation failed"
|
||||
log "Error details:"
|
||||
flask db migrate -m "Baseline from existing database" 2>&1 | head -20
|
||||
return 1
|
||||
fi
|
||||
log "✓ Baseline migration created"
|
||||
|
||||
# Stamp database as current
|
||||
if ! flask db stamp head; then
|
||||
log "✗ Database stamping failed"
|
||||
log "Error details:"
|
||||
flask db stamp head 2>&1 | head -20
|
||||
return 1
|
||||
fi
|
||||
log "✓ Database stamped as current"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Function to verify database integrity
|
||||
verify_database_integrity() {
|
||||
local db_url="$1"
|
||||
log "Verifying database integrity..."
|
||||
|
||||
# Try up to 3 times with delays
|
||||
local max_attempts=3
|
||||
local attempt=1
|
||||
|
||||
while [[ $attempt -le $max_attempts ]]; do
|
||||
log "Database integrity check attempt $attempt/$max_attempts..."
|
||||
|
||||
# Test basic database operations
|
||||
if [[ "$db_url" == postgresql* ]]; then
|
||||
log "Checking PostgreSQL database integrity..."
|
||||
if python -c "
|
||||
import psycopg2
|
||||
try:
|
||||
# Parse connection string to remove +psycopg2 if present
|
||||
conn_str = '$db_url'.replace('+psycopg2://', '://')
|
||||
conn = psycopg2.connect(conn_str)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check for key tables that should exist after migration
|
||||
cursor.execute(\"\"\"
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_name IN ('clients', 'users', 'projects', 'tasks', 'time_entries', 'settings', 'invoices', 'invoice_items')
|
||||
AND table_schema = 'public'
|
||||
ORDER BY table_name
|
||||
\"\"\")
|
||||
key_tables = [row[0] for row in cursor.fetchall()]
|
||||
|
||||
# Also check if alembic_version table exists
|
||||
cursor.execute(\"\"\"
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_name = 'alembic_version'
|
||||
AND table_schema = 'public'
|
||||
\"\"\")
|
||||
alembic_table = cursor.fetchone()
|
||||
|
||||
conn.close()
|
||||
|
||||
print(f'Found tables: {key_tables}')
|
||||
print(f'Alembic version table: {alembic_table[0] if alembic_table else \"missing\"}')
|
||||
|
||||
# Require at least the core tables and alembic_version
|
||||
if len(key_tables) >= 5 and alembic_table:
|
||||
exit(0)
|
||||
else:
|
||||
print(f'Expected at least 5 core tables, found {len(key_tables)}')
|
||||
print(f'Expected alembic_version table: {bool(alembic_table)}')
|
||||
exit(1)
|
||||
except Exception as e:
|
||||
print(f'Error during integrity check: {e}')
|
||||
exit(1)
|
||||
"; then
|
||||
log "✓ Database integrity verified (PostgreSQL via Python)"
|
||||
return 0
|
||||
else
|
||||
log "✗ Database integrity check failed (PostgreSQL)"
|
||||
log "Error details:"
|
||||
python -c "
|
||||
import psycopg2
|
||||
try:
|
||||
conn_str = '$db_url'.replace('+psycopg2://', '://')
|
||||
conn = psycopg2.connect(conn_str)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute(\"\"\"
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
ORDER BY table_name
|
||||
\"\"\")
|
||||
all_tables = [row[0] for row in cursor.fetchall()]
|
||||
|
||||
cursor.execute(\"\"\"
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_name = 'alembic_version'
|
||||
AND table_schema = 'public'
|
||||
\"\"\)
|
||||
alembic_table = cursor.fetchone()
|
||||
|
||||
conn.close()
|
||||
|
||||
print(f'All tables in database: {all_tables}')
|
||||
print(f'Alembic version table exists: {bool(alembic_table)}')
|
||||
except Exception as e:
|
||||
print(f'Error getting table list: {e}')
|
||||
" 2>&1 | head -20
|
||||
|
||||
if [[ $attempt -lt $max_attempts ]]; then
|
||||
log "Waiting 3 seconds before retry..."
|
||||
sleep 3
|
||||
attempt=$((attempt + 1))
|
||||
continue
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
elif [[ "$db_url" == sqlite://* ]]; then
|
||||
local db_file="${db_url#sqlite://}"
|
||||
|
||||
if [[ ! -f "$db_file" ]]; then
|
||||
log "✗ SQLite database file not found"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log "Checking SQLite database integrity..."
|
||||
if python -c "
|
||||
import sqlite3
|
||||
try:
|
||||
conn = sqlite3.connect('$db_file')
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('SELECT name FROM sqlite_master WHERE type=\"table\" AND name IN (\"clients\", \"users\", \"projects\", \"tasks\", \"time_entries\", \"settings\", \"invoices\", \"invoice_items\")')
|
||||
key_tables = [row[0] for row in cursor.fetchall()]
|
||||
|
||||
cursor.execute('SELECT name FROM sqlite_master WHERE type=\"table\" AND name=\"alembic_version\"')
|
||||
alembic_table = cursor.fetchone()
|
||||
|
||||
conn.close()
|
||||
|
||||
print(f'Found tables: {key_tables}')
|
||||
print(f'Alembic version table: {alembic_table[0] if alembic_table else \"missing\"}')
|
||||
|
||||
if len(key_tables) >= 5 and alembic_table:
|
||||
exit(0)
|
||||
else:
|
||||
print(f'Expected at least 5 core tables, found {len(key_tables)}')
|
||||
print(f'Expected alembic_version table: {bool(alembic_table)}')
|
||||
exit(1)
|
||||
except Exception as e:
|
||||
print(f'Error during integrity check: {e}')
|
||||
exit(1)
|
||||
"; then
|
||||
log "✓ Database integrity verified (SQLite)"
|
||||
return 0
|
||||
else
|
||||
log "✗ Database integrity check failed (SQLite)"
|
||||
if [[ $attempt -lt $max_attempts ]]; then
|
||||
log "Waiting 3 seconds before retry..."
|
||||
sleep 3
|
||||
attempt=$((attempt + 1))
|
||||
continue
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
log "✗ Unsupported database type: $db_url"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# If we get here, the check failed
|
||||
if [[ $attempt -lt $max_attempts ]]; then
|
||||
log "Waiting 3 seconds before retry..."
|
||||
sleep 3
|
||||
attempt=$((attempt + 1))
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Main execution
|
||||
main() {
|
||||
log "=== TimeTracker Docker Entrypoint with Migration Detection ==="
|
||||
|
||||
# Set environment variables
|
||||
export FLASK_APP=${FLASK_APP:-/app/app.py}
|
||||
|
||||
# Get database URL from environment
|
||||
local db_url="${DATABASE_URL}"
|
||||
if [[ -z "$db_url" ]]; then
|
||||
log "✗ DATABASE_URL environment variable not set"
|
||||
log "Available environment variables:"
|
||||
env | grep -E "(DATABASE|FLASK|PYTHON)" | sort
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "Database URL: $db_url"
|
||||
|
||||
# Wait for database to be available
|
||||
if ! wait_for_database "$db_url"; then
|
||||
log "✗ Failed to connect to database"
|
||||
log "Trying to run simple test script for debugging..."
|
||||
if [[ -f "/app/docker/simple_test.sh" ]]; then
|
||||
/app/docker/simple_test.sh
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Detect database state
|
||||
local db_state=$(detect_database_state "$db_url")
|
||||
log "Raw db_state output: '${db_state}'"
|
||||
log "Detected database state: '$db_state'"
|
||||
|
||||
# Choose migration strategy
|
||||
local strategy=$(choose_migration_strategy "$db_state")
|
||||
log "Raw strategy output: '${strategy}'"
|
||||
log "Selected migration strategy: '$strategy'"
|
||||
|
||||
# Log the strategy selection details
|
||||
case "$db_state" in
|
||||
"fresh")
|
||||
log "Fresh database detected - using standard initialization"
|
||||
;;
|
||||
"migrated")
|
||||
log "Database already migrated - checking for pending migrations"
|
||||
;;
|
||||
"legacy")
|
||||
log "Legacy database detected - using comprehensive migration"
|
||||
;;
|
||||
*)
|
||||
log "Unknown database state: '$db_state' - using comprehensive migration as fallback"
|
||||
;;
|
||||
esac
|
||||
|
||||
# Execute migration strategy
|
||||
if ! execute_migration_strategy "$strategy" "$db_url"; then
|
||||
log "✗ Migration strategy execution failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify database integrity
|
||||
if ! verify_database_integrity "$db_url"; then
|
||||
log "✗ Database integrity verification failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "=== Startup and Migration Complete ==="
|
||||
log "Database is ready for use"
|
||||
|
||||
# Show final migration status
|
||||
local final_status=$(flask db current 2>/dev/null | tr -d '\n' || echo "unknown")
|
||||
log "Final migration status: $final_status"
|
||||
|
||||
# Start the application
|
||||
log "Starting TimeTracker application..."
|
||||
exec "$@"
|
||||
}
|
||||
|
||||
# Run main function with all arguments
|
||||
main "$@"
|
||||
85
docker/simple_test.sh
Normal file
@@ -0,0 +1,85 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "=== Simple Test Script ==="
|
||||
echo "Timestamp: $(date)"
|
||||
echo "Container ID: $(hostname)"
|
||||
echo "Current directory: $(pwd)"
|
||||
echo "User: $(whoami)"
|
||||
echo
|
||||
|
||||
# Test 1: Basic environment
|
||||
echo "=== Test 1: Environment ==="
|
||||
echo "DATABASE_URL: ${DATABASE_URL:-'NOT SET'}"
|
||||
echo "FLASK_APP: ${FLASK_APP:-'NOT SET'}"
|
||||
echo "PATH: $PATH"
|
||||
echo
|
||||
|
||||
# Test 2: Basic commands
|
||||
echo "=== Test 2: Basic Commands ==="
|
||||
echo "Python version: $(python --version)"
|
||||
echo "Flask version: $(flask --version 2>/dev/null || echo 'Flask CLI not available')"
|
||||
echo "psql available: $(command -v psql >/dev/null 2>&1 && echo 'YES' || echo 'NO')"
|
||||
echo
|
||||
|
||||
# Test 3: File access
|
||||
echo "=== Test 3: File Access ==="
|
||||
echo "Can access /app: $(test -d /app && echo 'YES' || echo 'NO')"
|
||||
echo "Can access /app/docker: $(test -d /app/docker && echo 'YES' || echo 'NO')"
|
||||
echo "Entrypoint script exists: $(test -f /app/docker/entrypoint.sh && echo 'YES' || echo 'NO')"
|
||||
echo "Entrypoint script executable: $(test -x /app/docker/entrypoint.sh && echo 'YES' || echo 'NO')"
|
||||
echo
|
||||
|
||||
# Test 4: Network connectivity
|
||||
echo "=== Test 4: Network Connectivity ==="
|
||||
if ping -c 1 db >/dev/null 2>&1; then
|
||||
echo "✓ Can ping database host 'db'"
|
||||
else
|
||||
echo "✗ Cannot ping database host 'db'"
|
||||
fi
|
||||
|
||||
if command -v nc >/dev/null 2>&1; then
|
||||
if nc -zv db 5432 2>/dev/null; then
|
||||
echo "✓ Database port 5432 is accessible"
|
||||
else
|
||||
echo "✗ Database port 5432 is not accessible"
|
||||
fi
|
||||
else
|
||||
echo "⚠ netcat not available, skipping port test"
|
||||
fi
|
||||
echo
|
||||
|
||||
# Test 5: Python connection test
|
||||
echo "=== Test 5: Python Connection Test ==="
|
||||
if [[ -n "${DATABASE_URL}" ]]; then
|
||||
echo "Testing connection with Python..."
|
||||
if python -c "
|
||||
import psycopg2
|
||||
import sys
|
||||
try:
|
||||
# Parse connection string to remove +psycopg2 if present
|
||||
conn_str = '${DATABASE_URL}'.replace('+psycopg2://', '://')
|
||||
print(f'Trying to connect to: {conn_str}')
|
||||
conn = psycopg2.connect(conn_str)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT 1')
|
||||
result = cursor.fetchone()
|
||||
print(f'✓ Connection successful, test query result: {result}')
|
||||
conn.close()
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
print(f'✗ Connection failed: {e}')
|
||||
sys.exit(1)
|
||||
"; then
|
||||
echo "✓ Python connection test successful"
|
||||
else
|
||||
echo "✗ Python connection test failed"
|
||||
fi
|
||||
else
|
||||
echo "⚠ DATABASE_URL not set, skipping connection test"
|
||||
fi
|
||||
echo
|
||||
|
||||
echo "=== Test Complete ==="
|
||||
echo "If you see this message, the basic script execution is working."
|
||||
echo "Check the output above for any specific failures."
|
||||
382
docker/startup_with_migration.py
Normal file
@@ -0,0 +1,382 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Enhanced Startup Script with Automatic Migration Detection
|
||||
This script automatically detects database state and chooses the correct migration strategy
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import subprocess
|
||||
import sqlite3
|
||||
import psycopg2
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
import logging
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.StreamHandler(sys.stdout),
|
||||
logging.FileHandler('/var/log/timetracker_startup.log')
|
||||
]
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def wait_for_database(db_url, max_retries=60, retry_delay=3):
|
||||
"""Wait for database to be available"""
|
||||
logger.info(f"Waiting for database to be available: {db_url}")
|
||||
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
if db_url.startswith('postgresql'):
|
||||
# Handle both postgresql:// and postgresql+psycopg2:// URLs
|
||||
clean_url = db_url.replace('+psycopg2://', '://')
|
||||
conn = psycopg2.connect(clean_url)
|
||||
conn.close()
|
||||
logger.info("✓ PostgreSQL database is available")
|
||||
return True
|
||||
elif db_url.startswith('sqlite'):
|
||||
# For SQLite, just check if the file exists or can be created
|
||||
db_file = db_url.replace('sqlite:///', '')
|
||||
if os.path.exists(db_file) or os.access(os.path.dirname(db_file), os.W_OK):
|
||||
logger.info("✓ SQLite database is available")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.info(f"Database not ready (attempt {attempt + 1}/{max_retries}): {e}")
|
||||
if attempt < max_retries - 1:
|
||||
time.sleep(retry_delay)
|
||||
|
||||
logger.error("Database is not available after maximum retries")
|
||||
return False
|
||||
|
||||
def detect_database_state(db_url):
|
||||
"""Detect the current state of the database"""
|
||||
logger.info("Analyzing database state...")
|
||||
|
||||
try:
|
||||
if db_url.startswith('postgresql'):
|
||||
# Handle both postgresql:// and postgresql+psycopg2:// URLs
|
||||
clean_url = db_url.replace('+psycopg2://', '://')
|
||||
conn = psycopg2.connect(clean_url)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check if alembic_version table exists
|
||||
cursor.execute("""
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_name = 'alembic_version'
|
||||
)
|
||||
""")
|
||||
has_alembic = cursor.fetchone()[0]
|
||||
|
||||
# Get list of existing tables
|
||||
cursor.execute("""
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
ORDER BY table_name
|
||||
""")
|
||||
existing_tables = [row[0] for row in cursor.fetchall()]
|
||||
|
||||
# Check if this is a fresh database
|
||||
is_fresh = len(existing_tables) == 0
|
||||
|
||||
conn.close()
|
||||
|
||||
logger.info(f"Database state: has_alembic={has_alembic}, tables={existing_tables}, is_fresh={is_fresh}")
|
||||
|
||||
if has_alembic:
|
||||
return 'migrated', existing_tables
|
||||
elif existing_tables:
|
||||
return 'legacy', existing_tables
|
||||
else:
|
||||
return 'fresh', []
|
||||
|
||||
elif db_url.startswith('sqlite'):
|
||||
db_file = db_url.replace('sqlite:///', '')
|
||||
|
||||
if not os.path.exists(db_file):
|
||||
return 'fresh', []
|
||||
|
||||
conn = sqlite3.connect(db_file)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check if alembic_version table exists
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='alembic_version'")
|
||||
has_alembic = cursor.fetchone() is not None
|
||||
|
||||
# Get list of existing tables
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
|
||||
existing_tables = [row[0] for row in cursor.fetchall()]
|
||||
|
||||
conn.close()
|
||||
|
||||
logger.info(f"Database state: has_alembic={has_alembic}, tables={existing_tables}")
|
||||
|
||||
if has_alembic:
|
||||
return 'migrated', existing_tables
|
||||
elif existing_tables:
|
||||
return 'legacy', existing_tables
|
||||
else:
|
||||
return 'fresh', []
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error detecting database state: {e}")
|
||||
return 'unknown', []
|
||||
|
||||
return 'unknown', []
|
||||
|
||||
def choose_migration_strategy(db_state, existing_tables):
|
||||
"""Choose the appropriate migration strategy based on database state"""
|
||||
logger.info(f"Choosing migration strategy for state: {db_state}")
|
||||
|
||||
if db_state == 'fresh':
|
||||
logger.info("Fresh database detected - using standard initialization")
|
||||
return 'fresh_init'
|
||||
|
||||
elif db_state == 'migrated':
|
||||
logger.info("Database already migrated - checking for pending migrations")
|
||||
return 'check_migrations'
|
||||
|
||||
elif db_state == 'legacy':
|
||||
logger.info("Legacy database detected - using comprehensive migration")
|
||||
return 'comprehensive_migration'
|
||||
|
||||
else:
|
||||
logger.warning("Unknown database state - using comprehensive migration as fallback")
|
||||
return 'comprehensive_migration'
|
||||
|
||||
def execute_migration_strategy(strategy, db_url):
|
||||
"""Execute the chosen migration strategy"""
|
||||
logger.info(f"Executing migration strategy: {strategy}")
|
||||
|
||||
try:
|
||||
if strategy == 'fresh_init':
|
||||
return execute_fresh_init(db_url)
|
||||
elif strategy == 'check_migrations':
|
||||
return execute_check_migrations(db_url)
|
||||
elif strategy == 'comprehensive_migration':
|
||||
return execute_comprehensive_migration(db_url)
|
||||
else:
|
||||
logger.error(f"Unknown migration strategy: {strategy}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Error executing migration strategy: {e}")
|
||||
return False
|
||||
|
||||
def execute_fresh_init(db_url):
|
||||
"""Execute fresh database initialization"""
|
||||
logger.info("Executing fresh database initialization...")
|
||||
|
||||
try:
|
||||
# Initialize Flask-Migrate
|
||||
result = subprocess.run(['flask', 'db', 'init'],
|
||||
capture_output=True, text=True, check=True)
|
||||
logger.info("✓ Flask-Migrate initialized")
|
||||
|
||||
# Create initial migration
|
||||
result = subprocess.run(['flask', 'db', 'migrate', '-m', 'Initial database schema'],
|
||||
capture_output=True, text=True, check=True)
|
||||
logger.info("✓ Initial migration created")
|
||||
|
||||
# Apply migration
|
||||
result = subprocess.run(['flask', 'db', 'upgrade'],
|
||||
capture_output=True, text=True, check=True)
|
||||
logger.info("✓ Initial migration applied")
|
||||
|
||||
return True
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Fresh init failed: {e}")
|
||||
logger.error(f"STDOUT: {e.stdout}")
|
||||
logger.error(f"STDERR: {e.stderr}")
|
||||
return False
|
||||
|
||||
def execute_check_migrations(db_url):
|
||||
"""Check and apply any pending migrations"""
|
||||
logger.info("Checking for pending migrations...")
|
||||
|
||||
try:
|
||||
# Check current migration status
|
||||
result = subprocess.run(['flask', 'db', 'current'],
|
||||
capture_output=True, text=True, check=True)
|
||||
current_revision = result.stdout.strip()
|
||||
logger.info(f"Current migration revision: {current_revision}")
|
||||
|
||||
# Check for pending migrations
|
||||
result = subprocess.run(['flask', 'db', 'upgrade'],
|
||||
capture_output=True, text=True, check=True)
|
||||
logger.info("✓ Migrations checked and applied")
|
||||
|
||||
return True
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Migration check failed: {e}")
|
||||
return False
|
||||
|
||||
def execute_comprehensive_migration(db_url):
|
||||
"""Execute comprehensive migration for legacy databases"""
|
||||
logger.info("Executing comprehensive migration...")
|
||||
|
||||
try:
|
||||
# Run the comprehensive migration script
|
||||
migration_script = '/app/migrations/migrate_existing_database.py'
|
||||
|
||||
if os.path.exists(migration_script):
|
||||
result = subprocess.run(['python', migration_script],
|
||||
capture_output=True, text=True, check=True)
|
||||
logger.info("✓ Comprehensive migration completed")
|
||||
return True
|
||||
else:
|
||||
logger.warning("Comprehensive migration script not found, falling back to manual migration")
|
||||
return execute_manual_migration(db_url)
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Comprehensive migration failed: {e}")
|
||||
logger.error(f"STDOUT: {e.stdout}")
|
||||
logger.error(f"STDERR: {e.stderr}")
|
||||
return False
|
||||
|
||||
def execute_manual_migration(db_url):
|
||||
"""Execute manual migration as fallback"""
|
||||
logger.info("Executing manual migration fallback...")
|
||||
|
||||
try:
|
||||
# Initialize Flask-Migrate if not already done
|
||||
if not os.path.exists('/app/migrations/env.py'):
|
||||
result = subprocess.run(['flask', 'db', 'init'],
|
||||
capture_output=True, text=True, check=True)
|
||||
logger.info("✓ Flask-Migrate initialized")
|
||||
|
||||
# Create baseline migration
|
||||
result = subprocess.run(['flask', 'db', 'migrate', '-m', 'Baseline from existing database'],
|
||||
capture_output=True, text=True, check=True)
|
||||
logger.info("✓ Baseline migration created")
|
||||
|
||||
# Stamp database as current
|
||||
result = subprocess.run(['flask', 'db', 'stamp', 'head'],
|
||||
capture_output=True, text=True, check=True)
|
||||
logger.info("✓ Database stamped as current")
|
||||
|
||||
return True
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Manual migration failed: {e}")
|
||||
return False
|
||||
|
||||
def verify_database_integrity(db_url):
|
||||
"""Verify that the database is working correctly after migration"""
|
||||
logger.info("Verifying database integrity...")
|
||||
|
||||
try:
|
||||
# Test basic database operations
|
||||
if db_url.startswith('postgresql'):
|
||||
# Handle both postgresql:// and postgresql+psycopg2:// URLs
|
||||
clean_url = db_url.replace('+psycopg2://', '://')
|
||||
conn = psycopg2.connect(clean_url)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check if key tables exist
|
||||
cursor.execute("""
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_name IN ('users', 'projects', 'time_entries')
|
||||
AND table_schema = 'public'
|
||||
""")
|
||||
key_tables = [row[0] for row in cursor.fetchall()]
|
||||
|
||||
conn.close()
|
||||
|
||||
if len(key_tables) >= 2: # At least users and projects
|
||||
logger.info("✓ Database integrity verified")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"Missing key tables: {key_tables}")
|
||||
return False
|
||||
|
||||
elif db_url.startswith('sqlite'):
|
||||
db_file = db_url.replace('sqlite:///', '')
|
||||
|
||||
if not os.path.exists(db_file):
|
||||
logger.error("SQLite database file not found")
|
||||
return False
|
||||
|
||||
conn = sqlite3.connect(db_file)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check if key tables exist
|
||||
cursor.execute("""
|
||||
SELECT name FROM sqlite_master
|
||||
WHERE type='table' AND name IN ('users', 'projects', 'time_entries')
|
||||
""")
|
||||
key_tables = [row[0] for row in cursor.fetchall()]
|
||||
|
||||
conn.close()
|
||||
|
||||
if len(key_tables) >= 2: # At least users and projects
|
||||
logger.info("✓ Database integrity verified")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"Missing key tables: {key_tables}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Database integrity check failed: {e}")
|
||||
return False
|
||||
|
||||
return False
|
||||
|
||||
def main():
|
||||
"""Main startup function"""
|
||||
logger.info("=== TimeTracker Enhanced Startup with Migration Detection ===")
|
||||
|
||||
# Set environment variables
|
||||
os.environ.setdefault('FLASK_APP', '/app/app.py')
|
||||
|
||||
# Get database URL from environment
|
||||
db_url = os.getenv('DATABASE_URL')
|
||||
if not db_url:
|
||||
logger.error("DATABASE_URL environment variable not set")
|
||||
sys.exit(1)
|
||||
|
||||
logger.info(f"Database URL: {db_url}")
|
||||
|
||||
# Wait for database to be available
|
||||
if not wait_for_database(db_url):
|
||||
logger.error("Failed to connect to database")
|
||||
sys.exit(1)
|
||||
|
||||
# Detect database state
|
||||
db_state, existing_tables = detect_database_state(db_url)
|
||||
logger.info(f"Detected database state: {db_state} with {len(existing_tables)} tables")
|
||||
|
||||
# Choose migration strategy
|
||||
strategy = choose_migration_strategy(db_state, existing_tables)
|
||||
logger.info(f"Selected migration strategy: {strategy}")
|
||||
|
||||
# Execute migration strategy
|
||||
if not execute_migration_strategy(strategy, db_url):
|
||||
logger.error("Migration strategy execution failed")
|
||||
sys.exit(1)
|
||||
|
||||
# Verify database integrity
|
||||
if not verify_database_integrity(db_url):
|
||||
logger.error("Database integrity verification failed")
|
||||
sys.exit(1)
|
||||
|
||||
logger.info("=== Startup and Migration Complete ===")
|
||||
logger.info("Database is ready for use")
|
||||
|
||||
# Show final migration status
|
||||
try:
|
||||
result = subprocess.run(['flask', 'db', 'current'],
|
||||
capture_output=True, text=True, check=True)
|
||||
logger.info(f"Final migration status: {result.stdout.strip()}")
|
||||
except:
|
||||
logger.info("Could not determine final migration status")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
136
docker/test_db_connection.py
Normal file
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Simple Database Connection Test Script
|
||||
This script tests database connectivity to help debug connection issues
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import psycopg2
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
|
||||
def test_postgresql_connection(db_url):
|
||||
"""Test PostgreSQL connection"""
|
||||
print(f"Testing PostgreSQL connection: {db_url}")
|
||||
|
||||
try:
|
||||
# Handle both postgresql:// and postgresql+psycopg2:// URLs
|
||||
clean_url = db_url.replace('+psycopg2://', '://')
|
||||
print(f"Cleaned URL: {clean_url}")
|
||||
|
||||
# Test connection
|
||||
conn = psycopg2.connect(clean_url)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Test basic query
|
||||
cursor.execute("SELECT version()")
|
||||
version = cursor.fetchone()[0]
|
||||
print(f"✓ PostgreSQL connection successful")
|
||||
print(f" Server version: {version}")
|
||||
|
||||
# Test if we can access information_schema
|
||||
cursor.execute("SELECT current_database(), current_user")
|
||||
db_info = cursor.fetchone()
|
||||
print(f" Database: {db_info[0]}")
|
||||
print(f" User: {db_info[1]}")
|
||||
|
||||
# Check if we can list tables
|
||||
cursor.execute("""
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
ORDER BY table_name
|
||||
""")
|
||||
tables = [row[0] for row in cursor.fetchall()]
|
||||
print(f" Tables found: {len(tables)}")
|
||||
if tables:
|
||||
print(f" Table names: {tables[:5]}{'...' if len(tables) > 5 else ''}")
|
||||
|
||||
conn.close()
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ PostgreSQL connection failed: {e}")
|
||||
return False
|
||||
|
||||
def test_sqlite_connection(db_url):
|
||||
"""Test SQLite connection"""
|
||||
print(f"Testing SQLite connection: {db_url}")
|
||||
|
||||
try:
|
||||
db_file = db_url.replace('sqlite:///', '')
|
||||
print(f"Database file: {db_file}")
|
||||
|
||||
if not os.path.exists(db_file):
|
||||
print(f" Database file does not exist, checking if directory is writable...")
|
||||
dir_path = os.path.dirname(db_file) if os.path.dirname(db_file) else '.'
|
||||
if os.access(dir_path, os.W_OK):
|
||||
print(f" ✓ Directory is writable: {dir_path}")
|
||||
return True
|
||||
else:
|
||||
print(f" ✗ Directory is not writable: {dir_path}")
|
||||
return False
|
||||
|
||||
# Test connection
|
||||
conn = sqlite3.connect(db_file)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Test basic query
|
||||
cursor.execute("SELECT sqlite_version()")
|
||||
version = cursor.fetchone()[0]
|
||||
print(f"✓ SQLite connection successful")
|
||||
print(f" SQLite version: {version}")
|
||||
|
||||
# Check if we can list tables
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
|
||||
tables = [row[0] for row in cursor.fetchall()]
|
||||
print(f" Tables found: {len(tables)}")
|
||||
if tables:
|
||||
print(f" Table names: {tables[:5]}{'...' if len(tables) > 5 else ''}")
|
||||
|
||||
conn.close()
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ SQLite connection failed: {e}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
"""Main function"""
|
||||
print("=== Database Connection Test ===")
|
||||
print(f"Timestamp: {datetime.now()}")
|
||||
print()
|
||||
|
||||
# Get database URL from environment
|
||||
db_url = os.getenv('DATABASE_URL')
|
||||
if not db_url:
|
||||
print("✗ DATABASE_URL environment variable not set")
|
||||
print("Please set DATABASE_URL to test database connection")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Database URL: {db_url}")
|
||||
print()
|
||||
|
||||
# Test connection based on database type
|
||||
success = False
|
||||
|
||||
if db_url.startswith('postgresql'):
|
||||
success = test_postgresql_connection(db_url)
|
||||
elif db_url.startswith('sqlite'):
|
||||
success = test_sqlite_connection(db_url)
|
||||
else:
|
||||
print(f"✗ Unknown database type: {db_url}")
|
||||
print("Supported types: postgresql://, sqlite://")
|
||||
sys.exit(1)
|
||||
|
||||
print()
|
||||
if success:
|
||||
print("🎉 Database connection test successful!")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print("❌ Database connection test failed!")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
358
migrations/MIGRATION_GUIDE.md
Normal file
@@ -0,0 +1,358 @@
|
||||
# Complete Database Migration Guide
|
||||
|
||||
This guide covers **ALL** possible migration scenarios from any previous database state to the new Flask-Migrate system.
|
||||
|
||||
## 🎯 Migration Scenarios Covered
|
||||
|
||||
### 1. **Fresh Installation** (No existing database)
|
||||
- New project setup
|
||||
- First-time deployment
|
||||
|
||||
### 2. **Existing Database with Old Custom Migrations**
|
||||
- Legacy migration scripts
|
||||
- Custom Python migration files
|
||||
- Manual schema changes
|
||||
|
||||
### 3. **Existing Database with Mixed Schema**
|
||||
- Some tables exist, some missing
|
||||
- Incomplete schema
|
||||
- Partial migrations
|
||||
|
||||
### 4. **Existing Database with Data**
|
||||
- Production databases with user data
|
||||
- Development databases with test data
|
||||
- Staging databases
|
||||
|
||||
### 5. **Database from Different Versions**
|
||||
- Old TimeTracker versions
|
||||
- Different schema versions
|
||||
- Incompatible table structures
|
||||
|
||||
## 🚀 Migration Methods
|
||||
|
||||
### **Method 1: Automated Migration (Recommended)**
|
||||
|
||||
#### **For Any Existing Database:**
|
||||
```bash
|
||||
# Run the comprehensive migration script
|
||||
python migrations/migrate_existing_database.py
|
||||
```
|
||||
|
||||
This script will:
|
||||
- ✅ Detect your database type (PostgreSQL/SQLite)
|
||||
- ✅ Create a complete backup
|
||||
- ✅ Analyze existing schema
|
||||
- ✅ Create migration strategy
|
||||
- ✅ Initialize Flask-Migrate
|
||||
- ✅ Create baseline migration
|
||||
- ✅ Preserve all existing data
|
||||
- ✅ Verify migration success
|
||||
|
||||
#### **For Legacy Schema Migration:**
|
||||
```bash
|
||||
# Run the legacy schema migration script
|
||||
python migrations/legacy_schema_migration.py
|
||||
```
|
||||
|
||||
This script handles:
|
||||
- ✅ Old `projects.client` → `projects.client_id` conversion
|
||||
- ✅ Missing columns in settings table
|
||||
- ✅ Legacy table structures
|
||||
- ✅ Data preservation
|
||||
|
||||
### **Method 2: Manual Migration**
|
||||
|
||||
#### **Step-by-Step Process:**
|
||||
|
||||
1. **Backup Your Database**
|
||||
```bash
|
||||
# PostgreSQL
|
||||
pg_dump --format=custom --dbname="$DATABASE_URL" --file=backup_$(date +%Y%m%d_%H%M%S).dump
|
||||
|
||||
# SQLite
|
||||
cp instance/timetracker.db backup_timetracker_$(date +%Y%m%d_%H%M%S).db
|
||||
```
|
||||
|
||||
2. **Initialize Flask-Migrate**
|
||||
```bash
|
||||
flask db init
|
||||
```
|
||||
|
||||
3. **Create Baseline Migration**
|
||||
```bash
|
||||
flask db migrate -m "Baseline from existing database"
|
||||
```
|
||||
|
||||
4. **Review Generated Migration**
|
||||
```bash
|
||||
# Check the generated file in migrations/versions/
|
||||
cat migrations/versions/*.py
|
||||
```
|
||||
|
||||
5. **Stamp Database as Current**
|
||||
```bash
|
||||
flask db stamp head
|
||||
```
|
||||
|
||||
6. **Apply Any Pending Migrations**
|
||||
```bash
|
||||
flask db upgrade
|
||||
```
|
||||
|
||||
### **Method 3: Setup Scripts**
|
||||
|
||||
#### **Windows:**
|
||||
```bash
|
||||
scripts\setup-migrations.bat
|
||||
```
|
||||
|
||||
#### **Linux/Mac:**
|
||||
```bash
|
||||
scripts/setup-migrations.sh
|
||||
```
|
||||
|
||||
#### **Python (Cross-platform):**
|
||||
```bash
|
||||
python migrations/manage_migrations.py
|
||||
```
|
||||
|
||||
## 🔍 Pre-Migration Analysis
|
||||
|
||||
### **Check Your Current State:**
|
||||
|
||||
```bash
|
||||
# Check if Flask-Migrate is available
|
||||
python -c "import flask_migrate; print('✓ Flask-Migrate available')"
|
||||
|
||||
# Check database connection
|
||||
python -c "from app import create_app; app = create_app(); print('✓ Database accessible')"
|
||||
|
||||
# Check existing tables (if any)
|
||||
python -c "
|
||||
from app import create_app, db
|
||||
app = create_app()
|
||||
with app.app_context():
|
||||
inspector = db.inspect(db.engine)
|
||||
tables = inspector.get_table_names()
|
||||
print(f'Existing tables: {tables}')
|
||||
"
|
||||
```
|
||||
|
||||
### **Common Database States:**
|
||||
|
||||
| State | Description | Migration Approach |
|
||||
|-------|-------------|-------------------|
|
||||
| **No Database** | Fresh installation | `flask db init` → `flask db migrate` → `flask db upgrade` |
|
||||
| **Empty Database** | Tables exist but no data | `flask db stamp head` → `flask db upgrade` |
|
||||
| **Partial Schema** | Some tables missing | Run comprehensive migration script |
|
||||
| **Legacy Schema** | Old table structures | Run legacy schema migration script |
|
||||
| **Complete Schema** | All tables exist | `flask db stamp head` |
|
||||
|
||||
## 🛡️ Safety Measures
|
||||
|
||||
### **Automatic Backups:**
|
||||
- ✅ Database backup before any migration
|
||||
- ✅ Timestamped backup files
|
||||
- ✅ Multiple backup formats (dump, copy)
|
||||
- ✅ Backup verification
|
||||
|
||||
### **Migration Verification:**
|
||||
- ✅ Schema analysis before migration
|
||||
- ✅ Data integrity checks
|
||||
- ✅ Rollback capability
|
||||
- ✅ Migration status verification
|
||||
|
||||
### **Error Handling:**
|
||||
- ✅ Graceful failure handling
|
||||
- ✅ Detailed error messages
|
||||
- ✅ Recovery instructions
|
||||
- ✅ Safe fallback options
|
||||
|
||||
## 📋 Migration Checklist
|
||||
|
||||
### **Before Migration:**
|
||||
- [ ] **Backup your database** (automatic with scripts)
|
||||
- [ ] **Check disk space** (need space for backup + migration)
|
||||
- [ ] **Stop application** (if running)
|
||||
- [ ] **Verify dependencies** (Flask-Migrate installed)
|
||||
- [ ] **Check permissions** (database access)
|
||||
|
||||
### **During Migration:**
|
||||
- [ ] **Run migration script** (automatic or manual)
|
||||
- [ ] **Review generated files** (check migrations/versions/)
|
||||
- [ ] **Verify backup creation** (check backup files)
|
||||
- [ ] **Monitor progress** (watch for errors)
|
||||
|
||||
### **After Migration:**
|
||||
- [ ] **Test application** (start and verify functionality)
|
||||
- [ ] **Check migration status** (`flask db current`)
|
||||
- [ ] **Verify data integrity** (check key tables)
|
||||
- [ ] **Update deployment scripts** (if using CI/CD)
|
||||
|
||||
## 🔧 Troubleshooting
|
||||
|
||||
### **Common Issues & Solutions:**
|
||||
|
||||
#### **1. Migration Already Applied**
|
||||
```bash
|
||||
# Check current status
|
||||
flask db current
|
||||
|
||||
# If migration is already applied, stamp the database
|
||||
flask db stamp head
|
||||
```
|
||||
|
||||
#### **2. Schema Conflicts**
|
||||
```bash
|
||||
# Show migration heads
|
||||
flask db heads
|
||||
|
||||
# Merge branches if needed
|
||||
flask db merge -m "Merge migration branches" <revision1> <revision2>
|
||||
```
|
||||
|
||||
#### **3. Database Out of Sync**
|
||||
```bash
|
||||
# Check migration history
|
||||
flask db history
|
||||
|
||||
# Reset to specific revision
|
||||
flask db stamp <revision>
|
||||
```
|
||||
|
||||
#### **4. Permission Errors**
|
||||
```bash
|
||||
# Check database permissions
|
||||
# Ensure user has CREATE, ALTER, INSERT privileges
|
||||
# For PostgreSQL: GRANT ALL PRIVILEGES ON DATABASE timetracker TO timetracker;
|
||||
```
|
||||
|
||||
#### **5. Connection Issues**
|
||||
```bash
|
||||
# Verify DATABASE_URL environment variable
|
||||
echo $DATABASE_URL
|
||||
|
||||
# Test connection manually
|
||||
python -c "from app import create_app; app = create_app(); print('Connection OK')"
|
||||
```
|
||||
|
||||
### **Recovery Options:**
|
||||
|
||||
#### **If Migration Fails:**
|
||||
1. **Check backup files** - your data is safe
|
||||
2. **Review error logs** - identify the issue
|
||||
3. **Fix the problem** - resolve conflicts/errors
|
||||
4. **Restart migration** - run script again
|
||||
|
||||
#### **If Data is Lost:**
|
||||
1. **Restore from backup** - use pg_restore or copy SQLite file
|
||||
2. **Verify restoration** - check data integrity
|
||||
3. **Contact support** - if issues persist
|
||||
|
||||
## 📚 Advanced Migration Scenarios
|
||||
|
||||
### **Handling Complex Schema Changes:**
|
||||
|
||||
#### **Custom Data Migrations:**
|
||||
```python
|
||||
# In your migration file
|
||||
def upgrade():
|
||||
# Custom data transformation
|
||||
op.execute("UPDATE users SET role = 'user' WHERE role IS NULL")
|
||||
|
||||
# Complex table modifications
|
||||
op.execute("""
|
||||
INSERT INTO new_table (id, name, status)
|
||||
SELECT id, name, 'active' FROM old_table
|
||||
WHERE status = 'enabled'
|
||||
""")
|
||||
```
|
||||
|
||||
#### **Conditional Migrations:**
|
||||
```python
|
||||
def upgrade():
|
||||
# Handle different database types
|
||||
if op.get_bind().dialect.name == 'postgresql':
|
||||
op.execute('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"')
|
||||
elif op.get_bind().dialect.name == 'sqlite':
|
||||
# SQLite-specific operations
|
||||
pass
|
||||
```
|
||||
|
||||
#### **Rollback Strategies:**
|
||||
```python
|
||||
def downgrade():
|
||||
# Always provide rollback capability
|
||||
op.drop_table('new_table')
|
||||
op.drop_column('existing_table', 'new_column')
|
||||
```
|
||||
|
||||
## 🚀 Post-Migration
|
||||
|
||||
### **Verification Steps:**
|
||||
```bash
|
||||
# Check migration status
|
||||
flask db current
|
||||
|
||||
# View migration history
|
||||
flask db history
|
||||
|
||||
# Test database connection
|
||||
python -c "from app import create_app, db; app = create_app(); print('OK')"
|
||||
|
||||
# Verify key tables
|
||||
python -c "
|
||||
from app import create_app, db
|
||||
from app.models import User, Project, TimeEntry
|
||||
app = create_app()
|
||||
with app.app_context():
|
||||
users = User.query.count()
|
||||
projects = Project.query.count()
|
||||
entries = TimeEntry.query.count()
|
||||
print(f'Users: {users}, Projects: {projects}, Entries: {entries}')
|
||||
"
|
||||
```
|
||||
|
||||
### **Future Migrations:**
|
||||
```bash
|
||||
# Create new migration
|
||||
flask db migrate -m "Add new feature"
|
||||
|
||||
# Apply migration
|
||||
flask db upgrade
|
||||
|
||||
# Rollback if needed
|
||||
flask db downgrade
|
||||
```
|
||||
|
||||
## 📞 Support & Help
|
||||
|
||||
### **Getting Help:**
|
||||
1. **Check this guide** - covers most scenarios
|
||||
2. **Review migration logs** - detailed error information
|
||||
3. **Check Flask-Migrate docs** - https://flask-migrate.readthedocs.io/
|
||||
4. **Check Alembic docs** - https://alembic.sqlalchemy.org/
|
||||
|
||||
### **Emergency Recovery:**
|
||||
```bash
|
||||
# If everything fails, restore from backup
|
||||
# PostgreSQL:
|
||||
pg_restore --clean --dbname="$DATABASE_URL" backup_file.dump
|
||||
|
||||
# SQLite:
|
||||
cp backup_file.db instance/timetracker.db
|
||||
```
|
||||
|
||||
## 🎉 Success Indicators
|
||||
|
||||
Your migration is successful when:
|
||||
- ✅ `flask db current` shows a revision number
|
||||
- ✅ `flask db history` shows migration timeline
|
||||
- ✅ Application starts without database errors
|
||||
- ✅ All existing data is accessible
|
||||
- ✅ New features work correctly
|
||||
- ✅ Backup files are created and accessible
|
||||
|
||||
---
|
||||
|
||||
**Remember**: The migration scripts are designed to be **safe** and **reversible**. Your data is automatically backed up, and you can always restore if needed. When in doubt, run the comprehensive migration script - it handles all scenarios automatically!
|
||||
269
migrations/README.md
Normal file
@@ -0,0 +1,269 @@
|
||||
# Database Migrations with Flask-Migrate
|
||||
|
||||
This directory contains the database migration system for TimeTracker, now standardized on Flask-Migrate with proper versioning.
|
||||
|
||||
## Overview
|
||||
|
||||
The migration system has been updated from custom Python scripts to use Flask-Migrate, which provides:
|
||||
- **Standardized migrations** using Alembic
|
||||
- **Version tracking** for all database changes
|
||||
- **Rollback capabilities** to previous versions
|
||||
- **Automatic schema detection** from SQLAlchemy models
|
||||
- **Cross-database compatibility** (PostgreSQL, SQLite)
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Initialize Migrations (First Time Only)
|
||||
```bash
|
||||
flask db init
|
||||
```
|
||||
|
||||
### 2. Create Your First Migration
|
||||
```bash
|
||||
flask db migrate -m "Initial database schema"
|
||||
```
|
||||
|
||||
### 3. Apply Migrations
|
||||
```bash
|
||||
flask db upgrade
|
||||
```
|
||||
|
||||
## Migration Commands
|
||||
|
||||
### Basic Commands
|
||||
- `flask db init` - Initialize migrations directory
|
||||
- `flask db migrate -m "Description"` - Create a new migration
|
||||
- `flask db upgrade` - Apply pending migrations
|
||||
- `flask db downgrade` - Rollback last migration
|
||||
- `flask db current` - Show current migration version
|
||||
- `flask db history` - Show migration history
|
||||
|
||||
### Advanced Commands
|
||||
- `flask db show <revision>` - Show specific migration details
|
||||
- `flask db stamp <revision>` - Mark database as being at specific revision
|
||||
- `flask db heads` - Show current heads (for branched migrations)
|
||||
|
||||
## Migration Workflow
|
||||
|
||||
### 1. Make Model Changes
|
||||
Edit your SQLAlchemy models in `app/models/`:
|
||||
```python
|
||||
# Example: Add a new column
|
||||
class User(db.Model):
|
||||
# ... existing fields ...
|
||||
phone_number = db.Column(db.String(20), nullable=True)
|
||||
```
|
||||
|
||||
### 2. Generate Migration
|
||||
```bash
|
||||
flask db migrate -m "Add phone number to users"
|
||||
```
|
||||
|
||||
### 3. Review Generated Migration
|
||||
Check the generated migration file in `migrations/versions/`:
|
||||
```python
|
||||
def upgrade():
|
||||
op.add_column('users', sa.Column('phone_number', sa.String(length=20), nullable=True))
|
||||
|
||||
def downgrade():
|
||||
op.drop_column('users', 'phone_number')
|
||||
```
|
||||
|
||||
### 4. Apply Migration
|
||||
```bash
|
||||
flask db upgrade
|
||||
```
|
||||
|
||||
### 5. Verify Changes
|
||||
Check the migration status:
|
||||
```bash
|
||||
flask db current
|
||||
```
|
||||
|
||||
## Migration Files Structure
|
||||
|
||||
```
|
||||
migrations/
|
||||
├── versions/ # Migration files
|
||||
│ ├── 001_initial_schema.py
|
||||
│ ├── 002_add_phone_number.py
|
||||
│ └── ...
|
||||
├── env.py # Migration environment
|
||||
├── script.py.mako # Migration template
|
||||
├── alembic.ini # Alembic configuration
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Transition from Old System
|
||||
|
||||
If you're migrating from the old custom migration system:
|
||||
|
||||
### 1. Backup Your Database
|
||||
```bash
|
||||
# PostgreSQL
|
||||
pg_dump --format=custom --dbname="$DATABASE_URL" --file=backup_$(date +%Y%m%d_%H%M%S).dump
|
||||
|
||||
# SQLite
|
||||
cp instance/timetracker.db backup_timetracker_$(date +%Y%m%d_%H%M%S).db
|
||||
```
|
||||
|
||||
### 2. Use Migration Management Script
|
||||
```bash
|
||||
python migrations/manage_migrations.py
|
||||
```
|
||||
|
||||
### 3. Or Manual Migration
|
||||
```bash
|
||||
# Initialize Flask-Migrate
|
||||
flask db init
|
||||
|
||||
# Create initial migration (captures current schema)
|
||||
flask db migrate -m "Initial schema from existing database"
|
||||
|
||||
# Apply migration
|
||||
flask db upgrade
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Migration Naming
|
||||
Use descriptive names for migrations:
|
||||
```bash
|
||||
flask db migrate -m "Add user profile fields"
|
||||
flask db migrate -m "Create project categories table"
|
||||
flask db migrate -m "Add invoice payment tracking"
|
||||
```
|
||||
|
||||
### 2. Testing Migrations
|
||||
Always test migrations on a copy of your production data:
|
||||
```bash
|
||||
# Test upgrade
|
||||
flask db upgrade
|
||||
|
||||
# Test downgrade
|
||||
flask db downgrade
|
||||
|
||||
# Verify data integrity
|
||||
```
|
||||
|
||||
### 3. Backup Before Migrations
|
||||
```bash
|
||||
# Always backup before major migrations
|
||||
flask db backup # Custom command
|
||||
# or
|
||||
pg_dump --format=custom --dbname="$DATABASE_URL" --file=pre_migration_backup.dump
|
||||
```
|
||||
|
||||
### 4. Review Generated Code
|
||||
Always review auto-generated migrations before applying:
|
||||
- Check the `upgrade()` function
|
||||
- Verify the `downgrade()` function
|
||||
- Ensure data types and constraints are correct
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### 1. Migration Already Applied
|
||||
```bash
|
||||
# Check current status
|
||||
flask db current
|
||||
|
||||
# If migration is already applied, stamp the database
|
||||
flask db stamp <revision>
|
||||
```
|
||||
|
||||
#### 2. Migration Conflicts
|
||||
```bash
|
||||
# Show migration heads
|
||||
flask db heads
|
||||
|
||||
# Merge branches if needed
|
||||
flask db merge -m "Merge migration branches" <revision1> <revision2>
|
||||
```
|
||||
|
||||
#### 3. Database Out of Sync
|
||||
```bash
|
||||
# Check migration history
|
||||
flask db history
|
||||
|
||||
# Reset to specific revision
|
||||
flask db stamp <revision>
|
||||
```
|
||||
|
||||
#### 4. Model Import Errors
|
||||
Ensure all models are imported in your application:
|
||||
```python
|
||||
# In app/__init__.py or similar
|
||||
from app.models import User, Project, TimeEntry, Task, Settings, Invoice, Client
|
||||
```
|
||||
|
||||
### Getting Help
|
||||
|
||||
1. Check the migration status: `flask db current`
|
||||
2. Review migration history: `flask db history`
|
||||
3. Check Alembic logs for detailed error messages
|
||||
4. Verify database connection and permissions
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Custom Migration Operations
|
||||
You can add custom operations in your migrations:
|
||||
```python
|
||||
def upgrade():
|
||||
# Custom data migration
|
||||
op.execute("UPDATE users SET role = 'user' WHERE role IS NULL")
|
||||
|
||||
# Custom table operations
|
||||
op.create_index('custom_idx', 'table_name', ['column_name'])
|
||||
```
|
||||
|
||||
### Data Migrations
|
||||
For complex data migrations, use the `op.execute()` method:
|
||||
```python
|
||||
def upgrade():
|
||||
# Migrate existing data
|
||||
op.execute("""
|
||||
INSERT INTO new_table (id, name)
|
||||
SELECT id, name FROM old_table
|
||||
""")
|
||||
```
|
||||
|
||||
### Conditional Migrations
|
||||
Handle different database types:
|
||||
```python
|
||||
def upgrade():
|
||||
# PostgreSQL-specific operations
|
||||
if op.get_bind().dialect.name == 'postgresql':
|
||||
op.execute('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"')
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Ensure these environment variables are set:
|
||||
```bash
|
||||
export FLASK_APP=app.py
|
||||
export DATABASE_URL="postgresql://user:pass@localhost/dbname"
|
||||
# or
|
||||
export DATABASE_URL="sqlite:///instance/timetracker.db"
|
||||
```
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
For automated deployments, include migration steps:
|
||||
```yaml
|
||||
# Example GitHub Actions step
|
||||
- name: Run database migrations
|
||||
run: |
|
||||
flask db upgrade
|
||||
env:
|
||||
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
||||
```
|
||||
|
||||
## Support
|
||||
|
||||
For migration-related issues:
|
||||
1. Check this README
|
||||
2. Review Flask-Migrate documentation: https://flask-migrate.readthedocs.io/
|
||||
3. Check Alembic documentation: https://alembic.sqlalchemy.org/
|
||||
4. Review generated migration files for errors
|
||||
107
migrations/alembic.ini
Normal file
@@ -0,0 +1,107 @@
|
||||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# path to migration scripts
|
||||
script_location = migrations
|
||||
|
||||
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
||||
# Uncomment the line below if you want the files to be prepended with date and time
|
||||
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
|
||||
|
||||
# sys.path path, will be prepended to sys.path if present.
|
||||
# defaults to the current working directory.
|
||||
prepend_sys_path = .
|
||||
|
||||
# timezone to use when rendering the date within the migration file
|
||||
# as well as the filename.
|
||||
# If specified, requires the python-dateutil library that can be
|
||||
# installed by adding `alembic[tz]` to the pip requirements
|
||||
# string value is passed to dateutil.tz.gettz()
|
||||
# leave blank for localtime
|
||||
# timezone =
|
||||
|
||||
# max length of characters to apply to the
|
||||
# "slug" field
|
||||
# truncate_slug_length = 40
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
# set to 'true' to allow .pyc and .pyo files without
|
||||
# a source .py file to be detected as revisions in the
|
||||
# versions/ directory
|
||||
# sourceless = false
|
||||
|
||||
# version number format
|
||||
version_num_format = %04d
|
||||
|
||||
# version path separator; As mentioned above, this is the character used to split
|
||||
# version_locations. The default within new alembic.ini files is "os", which uses
|
||||
# os.pathsep. If this key is omitted entirely, it falls back to the legacy
|
||||
# behavior of splitting on spaces and/or commas.
|
||||
# Valid values for version_path_separator are:
|
||||
#
|
||||
# version_path_separator = :
|
||||
# version_path_separator = ;
|
||||
# version_path_separator = space
|
||||
version_path_separator = os
|
||||
|
||||
# the output encoding used when revision files
|
||||
# are written from script.py.mako
|
||||
# output_encoding = utf-8
|
||||
|
||||
sqlalchemy.url = driver://user:pass@localhost/dbname
|
||||
|
||||
|
||||
[post_write_hooks]
|
||||
# post_write_hooks defines scripts or Python functions that are run
|
||||
# on newly generated revision scripts. See the documentation for further
|
||||
# detail and examples
|
||||
|
||||
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
||||
# hooks = black
|
||||
# black.type = console_scripts
|
||||
# black.entrypoint = black
|
||||
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
||||
|
||||
# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
|
||||
# hooks = ruff
|
||||
# ruff.type = exec
|
||||
# ruff.executable = %(here)s/.venv/bin/ruff
|
||||
# ruff.options = --fix REVISION_SCRIPT_FILENAME
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
||||
91
migrations/env.py
Normal file
@@ -0,0 +1,91 @@
|
||||
from __future__ import with_statement
|
||||
|
||||
import logging
|
||||
from logging.config import fileConfig
|
||||
|
||||
from flask import current_app
|
||||
|
||||
from alembic import context
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
fileConfig(config.config_file_name)
|
||||
logger = logging.getLogger('alembic.env')
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.metadata
|
||||
config.set_main_option(
|
||||
'sqlalchemy.url',
|
||||
str(current_app.extensions['migrate'].db.get_engine().url).replace(
|
||||
'%', '%%'))
|
||||
target_metadata = current_app.extensions['migrate'].db.metadata
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
|
||||
|
||||
def run_migrations_offline():
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url, target_metadata=target_metadata, literal_binds=True
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online():
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
|
||||
# this callback is used to prevent an auto-migration from being generated
|
||||
# when there are no changes to the schema
|
||||
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
|
||||
def process_revision_directives(context, revision, directives):
|
||||
if getattr(config.cmd_opts, 'autogenerate', False):
|
||||
script = directives[0]
|
||||
if script.upgrade_ops.is_empty():
|
||||
directives[:] = []
|
||||
logger.info('No changes in schema detected.')
|
||||
|
||||
connectable = current_app.extensions['migrate'].db.get_engine()
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(
|
||||
connection=connection,
|
||||
target_metadata=target_metadata,
|
||||
process_revision_directives=process_revision_directives,
|
||||
**current_app.extensions['migrate'].configure_args
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
||||
192
migrations/legacy_schema_migration.py
Normal file
@@ -0,0 +1,192 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Legacy Schema Migration Script for TimeTracker
|
||||
This script handles migration from old custom migration system to Flask-Migrate
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
def migrate_legacy_schema():
|
||||
"""Migrate from legacy schema to new Flask-Migrate system"""
|
||||
print("=== Legacy Schema Migration ===")
|
||||
|
||||
# Check if we're in the right directory
|
||||
if not Path("app.py").exists():
|
||||
print("Error: Please run this script from the TimeTracker root directory")
|
||||
return False
|
||||
|
||||
# Set environment variables
|
||||
os.environ.setdefault('FLASK_APP', 'app.py')
|
||||
|
||||
try:
|
||||
from app import create_app, db
|
||||
app = create_app()
|
||||
|
||||
with app.app_context():
|
||||
print("✓ Application context created")
|
||||
|
||||
# Check current database state
|
||||
inspector = db.inspect(db.engine)
|
||||
existing_tables = inspector.get_table_names()
|
||||
|
||||
print(f"Found {len(existing_tables)} existing tables: {existing_tables}")
|
||||
|
||||
# Handle legacy schema issues
|
||||
if 'projects' in existing_tables:
|
||||
migrate_projects_table(db)
|
||||
|
||||
if 'settings' in existing_tables:
|
||||
migrate_settings_table(db)
|
||||
|
||||
print("✓ Legacy schema migration completed")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Error during legacy schema migration: {e}")
|
||||
return False
|
||||
|
||||
def migrate_projects_table(db):
|
||||
"""Migrate projects table from legacy schema"""
|
||||
print("Migrating projects table...")
|
||||
|
||||
try:
|
||||
# Check if projects table has old 'client' column
|
||||
inspector = db.inspect(db.engine)
|
||||
project_columns = [col['name'] for col in inspector.get_columns('projects')]
|
||||
|
||||
if 'client' in project_columns and 'client_id' not in project_columns:
|
||||
print(" - Converting projects.client to projects.client_id")
|
||||
|
||||
# Create clients table if it doesn't exist
|
||||
if 'clients' not in inspector.get_table_names():
|
||||
print(" - Creating clients table")
|
||||
db.session.execute(db.text("""
|
||||
CREATE TABLE IF NOT EXISTS clients (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(200) UNIQUE NOT NULL,
|
||||
description TEXT,
|
||||
contact_person VARCHAR(200),
|
||||
email VARCHAR(200),
|
||||
phone VARCHAR(50),
|
||||
address TEXT,
|
||||
default_hourly_rate NUMERIC(9, 2),
|
||||
status VARCHAR(20) DEFAULT 'active' NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||
)
|
||||
"""))
|
||||
|
||||
# Add client_id column
|
||||
db.session.execute(db.text("""
|
||||
ALTER TABLE projects ADD COLUMN IF NOT EXISTS client_id INTEGER
|
||||
"""))
|
||||
|
||||
# Migrate existing client names to clients table
|
||||
db.session.execute(db.text("""
|
||||
INSERT INTO clients (name, status)
|
||||
SELECT DISTINCT client, 'active' FROM projects
|
||||
WHERE client IS NOT NULL AND client <> ''
|
||||
ON CONFLICT (name) DO NOTHING
|
||||
"""))
|
||||
|
||||
# Update projects to reference clients
|
||||
db.session.execute(db.text("""
|
||||
UPDATE projects p
|
||||
SET client_id = c.id
|
||||
FROM clients c
|
||||
WHERE p.client_id IS NULL AND p.client = c.name
|
||||
"""))
|
||||
|
||||
# Create index
|
||||
db.session.execute(db.text("""
|
||||
CREATE INDEX IF NOT EXISTS idx_projects_client_id ON projects(client_id)
|
||||
"""))
|
||||
|
||||
print(" - Projects table migration completed")
|
||||
|
||||
except Exception as e:
|
||||
print(f" - Warning: Projects table migration failed: {e}")
|
||||
|
||||
def migrate_settings_table(db):
|
||||
"""Migrate settings table from legacy schema"""
|
||||
print("Migrating settings table...")
|
||||
|
||||
try:
|
||||
inspector = db.inspect(db.engine)
|
||||
settings_columns = [col['name'] for col in inspector.get_columns('settings')]
|
||||
|
||||
# Add missing columns that the new system expects
|
||||
missing_columns = [
|
||||
('allow_analytics', 'BOOLEAN DEFAULT TRUE'),
|
||||
('logo_path', 'VARCHAR(500)'),
|
||||
('company_website', 'VARCHAR(200)')
|
||||
]
|
||||
|
||||
for col_name, col_def in missing_columns:
|
||||
if col_name not in settings_columns:
|
||||
print(f" - Adding missing column: {col_name}")
|
||||
db.session.execute(db.text(f"""
|
||||
ALTER TABLE settings ADD COLUMN {col_name} {col_def}
|
||||
"""))
|
||||
|
||||
print(" - Settings table migration completed")
|
||||
|
||||
except Exception as e:
|
||||
print(f" - Warning: Settings table migration failed: {e}")
|
||||
|
||||
def create_migration_baseline():
|
||||
"""Create a migration baseline for the current database state"""
|
||||
print("Creating migration baseline...")
|
||||
|
||||
try:
|
||||
# Initialize Flask-Migrate if not already done
|
||||
if not Path("migrations/env.py").exists():
|
||||
print(" - Initializing Flask-Migrate")
|
||||
os.system("flask db init")
|
||||
|
||||
# Create initial migration
|
||||
print(" - Creating initial migration")
|
||||
os.system('flask db migrate -m "Baseline from legacy schema"')
|
||||
|
||||
# Stamp database as being at this revision
|
||||
print(" - Stamping database")
|
||||
os.system("flask db stamp head")
|
||||
|
||||
print("✓ Migration baseline created")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Error creating migration baseline: {e}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
"""Main function"""
|
||||
print("=== TimeTracker Legacy Schema Migration ===")
|
||||
print("This script migrates from the old custom migration system to Flask-Migrate")
|
||||
|
||||
# Step 1: Migrate legacy schema
|
||||
if not migrate_legacy_schema():
|
||||
print("Legacy schema migration failed")
|
||||
sys.exit(1)
|
||||
|
||||
# Step 2: Create migration baseline
|
||||
if not create_migration_baseline():
|
||||
print("Migration baseline creation failed")
|
||||
sys.exit(1)
|
||||
|
||||
print("\n=== Migration Complete ===")
|
||||
print("🎉 Your legacy database has been successfully migrated!")
|
||||
|
||||
print("\nNext steps:")
|
||||
print("1. Test your application to ensure everything works")
|
||||
print("2. Review the generated migration files in migrations/versions/")
|
||||
print("3. For future schema changes, use:")
|
||||
print(" - flask db migrate -m 'Description of changes'")
|
||||
print(" - flask db upgrade")
|
||||
print("4. To check status: flask db current")
|
||||
print("5. To view history: flask db history")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
161
migrations/manage_migrations.py
Normal file
@@ -0,0 +1,161 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Migration Management Script for TimeTracker
|
||||
This script helps manage the transition from custom migrations to Flask-Migrate
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
def run_command(command, description):
|
||||
"""Run a command and handle errors"""
|
||||
print(f"\n--- {description} ---")
|
||||
print(f"Running: {command}")
|
||||
|
||||
try:
|
||||
result = subprocess.run(command, shell=True, check=True, capture_output=True, text=True)
|
||||
print(f"✓ {description} completed successfully")
|
||||
if result.stdout:
|
||||
print(result.stdout)
|
||||
return True
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"✗ {description} failed:")
|
||||
print(f"Error code: {e.returncode}")
|
||||
if e.stdout:
|
||||
print(f"STDOUT: {e.stdout}")
|
||||
if e.stderr:
|
||||
print(f"STDERR: {e.stderr}")
|
||||
return False
|
||||
|
||||
def check_flask_migrate_installed():
|
||||
"""Check if Flask-Migrate is properly installed"""
|
||||
try:
|
||||
import flask_migrate
|
||||
print("✓ Flask-Migrate is installed")
|
||||
return True
|
||||
except ImportError:
|
||||
print("✗ Flask-Migrate is not installed")
|
||||
print("Please install it with: pip install Flask-Migrate")
|
||||
return False
|
||||
|
||||
def initialize_migrations():
|
||||
"""Initialize Flask-Migrate if not already initialized"""
|
||||
migrations_dir = Path("migrations")
|
||||
|
||||
if not migrations_dir.exists():
|
||||
print("Initializing Flask-Migrate...")
|
||||
return run_command("flask db init", "Initialize Flask-Migrate")
|
||||
else:
|
||||
print("✓ Migrations directory already exists")
|
||||
return True
|
||||
|
||||
def create_initial_migration():
|
||||
"""Create the initial migration if it doesn't exist"""
|
||||
versions_dir = Path("migrations/versions")
|
||||
|
||||
if not versions_dir.exists() or not list(versions_dir.glob("*.py")):
|
||||
print("Creating initial migration...")
|
||||
return run_command("flask db migrate -m 'Initial database schema'", "Create initial migration")
|
||||
else:
|
||||
print("✓ Initial migration already exists")
|
||||
return True
|
||||
|
||||
def apply_migrations():
|
||||
"""Apply all pending migrations"""
|
||||
return run_command("flask db upgrade", "Apply database migrations")
|
||||
|
||||
def show_migration_status():
|
||||
"""Show current migration status"""
|
||||
return run_command("flask db current", "Show current migration")
|
||||
|
||||
def show_migration_history():
|
||||
"""Show migration history"""
|
||||
return run_command("flask db history", "Show migration history")
|
||||
|
||||
def backup_database():
|
||||
"""Create a backup of the current database"""
|
||||
print("\n--- Creating Database Backup ---")
|
||||
|
||||
# Check if we're using PostgreSQL or SQLite
|
||||
db_url = os.getenv('DATABASE_URL', '')
|
||||
|
||||
if db_url.startswith('postgresql'):
|
||||
print("PostgreSQL database detected")
|
||||
backup_cmd = "pg_dump --format=custom --dbname=\"$DATABASE_URL\" --file=backup_$(date +%Y%m%d_%H%M%S).dump"
|
||||
print(f"Please run: {backup_cmd}")
|
||||
return True
|
||||
else:
|
||||
print("SQLite database detected")
|
||||
# For SQLite, we'll just copy the file
|
||||
if os.path.exists('instance/timetracker.db'):
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
backup_file = f"backup_timetracker_{timestamp}.db"
|
||||
shutil.copy2('instance/timetracker.db', backup_file)
|
||||
print(f"✓ Database backed up to: {backup_file}")
|
||||
return True
|
||||
else:
|
||||
print("SQLite database file not found")
|
||||
return False
|
||||
|
||||
def main():
|
||||
"""Main migration management function"""
|
||||
print("=== TimeTracker Migration Management ===")
|
||||
print("This script will help you transition to Flask-Migrate")
|
||||
|
||||
# Check prerequisites
|
||||
if not check_flask_migrate_installed():
|
||||
sys.exit(1)
|
||||
|
||||
# Create backup
|
||||
print("\n⚠️ IMPORTANT: Creating database backup before proceeding...")
|
||||
if not backup_database():
|
||||
print("Failed to create backup. Please create one manually before proceeding.")
|
||||
response = input("Continue anyway? (y/N): ")
|
||||
if response.lower() != 'y':
|
||||
print("Migration cancelled.")
|
||||
sys.exit(1)
|
||||
|
||||
# Initialize migrations
|
||||
if not initialize_migrations():
|
||||
print("Failed to initialize migrations")
|
||||
sys.exit(1)
|
||||
|
||||
# Create initial migration
|
||||
if not create_initial_migration():
|
||||
print("Failed to create initial migration")
|
||||
sys.exit(1)
|
||||
|
||||
# Show current status
|
||||
show_migration_status()
|
||||
|
||||
# Apply migrations
|
||||
print("\n⚠️ About to apply migrations to your database...")
|
||||
response = input("Continue? (y/N): ")
|
||||
if response.lower() != 'y':
|
||||
print("Migration cancelled. You can run 'flask db upgrade' manually later.")
|
||||
return
|
||||
|
||||
if not apply_migrations():
|
||||
print("Failed to apply migrations")
|
||||
sys.exit(1)
|
||||
|
||||
# Show final status
|
||||
print("\n=== Migration Complete ===")
|
||||
show_migration_status()
|
||||
show_migration_history()
|
||||
|
||||
print("\n🎉 Migration to Flask-Migrate completed successfully!")
|
||||
print("\nNext steps:")
|
||||
print("1. Test your application to ensure everything works")
|
||||
print("2. For future schema changes, use:")
|
||||
print(" - flask db migrate -m 'Description of changes'")
|
||||
print(" - flask db upgrade")
|
||||
print("3. To rollback: flask db downgrade")
|
||||
print("4. To check status: flask db current")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
383
migrations/migrate_existing_database.py
Normal file
@@ -0,0 +1,383 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Comprehensive Database Migration Script for TimeTracker
|
||||
This script can migrate ANY existing database to the new Flask-Migrate system
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import sqlite3
|
||||
import psycopg2
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
import shutil
|
||||
|
||||
def run_command(command, description, capture_output=True):
|
||||
"""Run a command and handle errors"""
|
||||
print(f"\n--- {description} ---")
|
||||
print(f"Running: {command}")
|
||||
|
||||
try:
|
||||
if capture_output:
|
||||
result = subprocess.run(command, shell=True, check=True, capture_output=True, text=True)
|
||||
print(f"✓ {description} completed successfully")
|
||||
if result.stdout:
|
||||
print(result.stdout)
|
||||
return True
|
||||
else:
|
||||
subprocess.run(command, shell=True, check=True)
|
||||
print(f"✓ {description} completed successfully")
|
||||
return True
|
||||
except subprocess.CmdProcessError as e:
|
||||
print(f"✗ {description} failed:")
|
||||
print(f"Error code: {e.returncode}")
|
||||
if e.stdout:
|
||||
print(f"STDOUT: {e.stdout}")
|
||||
if e.stderr:
|
||||
print(f"STDERR: {e.stderr}")
|
||||
return False
|
||||
|
||||
def check_flask_migrate_installed():
|
||||
"""Check if Flask-Migrate is properly installed"""
|
||||
try:
|
||||
import flask_migrate
|
||||
print("✓ Flask-Migrate is installed")
|
||||
return True
|
||||
except ImportError:
|
||||
print("✗ Flask-Migrate is not installed")
|
||||
print("Please install it with: pip install Flask-Migrate")
|
||||
return False
|
||||
|
||||
def detect_database_type():
|
||||
"""Detect the type of database being used"""
|
||||
db_url = os.getenv('DATABASE_URL', '')
|
||||
|
||||
if db_url.startswith('postgresql'):
|
||||
return 'postgresql', db_url
|
||||
elif db_url.startswith('sqlite'):
|
||||
return 'sqlite', db_url
|
||||
else:
|
||||
# Check for common database files
|
||||
if os.path.exists('instance/timetracker.db'):
|
||||
return 'sqlite', 'sqlite:///instance/timetracker.db'
|
||||
elif os.path.exists('timetracker.db'):
|
||||
return 'sqlite', 'sqlite:///timetracker.db'
|
||||
else:
|
||||
return 'unknown', None
|
||||
|
||||
def backup_database(db_type, db_url):
|
||||
"""Create a comprehensive backup of the current database"""
|
||||
print("\n--- Creating Database Backup ---")
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
|
||||
if db_type == 'postgresql':
|
||||
backup_file = f"backup_postgresql_{timestamp}.dump"
|
||||
backup_cmd = f'pg_dump --format=custom --dbname="{db_url}" --file={backup_file}'
|
||||
print(f"PostgreSQL database detected")
|
||||
print(f"Running: {backup_cmd}")
|
||||
|
||||
try:
|
||||
subprocess.run(backup_cmd, shell=True, check=True)
|
||||
print(f"✓ Database backed up to: {backup_file}")
|
||||
return backup_file
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"✗ PostgreSQL backup failed: {e}")
|
||||
print("Please ensure pg_dump is available and you have proper permissions")
|
||||
return None
|
||||
|
||||
elif db_type == 'sqlite':
|
||||
# Find the actual database file
|
||||
if 'instance/timetracker.db' in db_url:
|
||||
db_file = 'instance/timetracker.db'
|
||||
elif 'timetracker.db' in db_url:
|
||||
db_file = 'timetracker.db'
|
||||
else:
|
||||
db_file = db_url.replace('sqlite:///', '')
|
||||
|
||||
if os.path.exists(db_file):
|
||||
backup_file = f"backup_sqlite_{timestamp}.db"
|
||||
shutil.copy2(db_file, backup_file)
|
||||
print(f"✓ SQLite database backed up to: {backup_file}")
|
||||
return backup_file
|
||||
else:
|
||||
print(f"✗ SQLite database file not found: {db_file}")
|
||||
return None
|
||||
else:
|
||||
print("✗ Unknown database type")
|
||||
return None
|
||||
|
||||
def analyze_existing_schema(db_type, db_url):
|
||||
"""Analyze the existing database schema to understand what needs to be migrated"""
|
||||
print("\n--- Analyzing Existing Database Schema ---")
|
||||
|
||||
if db_type == 'postgresql':
|
||||
try:
|
||||
conn = psycopg2.connect(db_url)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Get list of existing tables
|
||||
cursor.execute("""
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
ORDER BY table_name
|
||||
""")
|
||||
existing_tables = [row[0] for row in cursor.fetchall()]
|
||||
|
||||
# Get table schemas
|
||||
table_schemas = {}
|
||||
for table in existing_tables:
|
||||
cursor.execute(f"""
|
||||
SELECT column_name, data_type, is_nullable, column_default
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = '{table}'
|
||||
ORDER BY ordinal_position
|
||||
""")
|
||||
columns = cursor.fetchall()
|
||||
table_schemas[table] = columns
|
||||
|
||||
conn.close()
|
||||
|
||||
print(f"✓ Found {len(existing_tables)} existing tables: {existing_tables}")
|
||||
return existing_tables, table_schemas
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Error analyzing PostgreSQL schema: {e}")
|
||||
return [], {}
|
||||
|
||||
elif db_type == 'sqlite':
|
||||
try:
|
||||
# Find the actual database file
|
||||
if 'instance/timetracker.db' in db_url:
|
||||
db_file = 'instance/timetracker.db'
|
||||
elif 'timetracker.db' in db_url:
|
||||
db_file = 'timetracker.db'
|
||||
else:
|
||||
db_file = db_url.replace('sqlite:///', '')
|
||||
|
||||
if not os.path.exists(db_file):
|
||||
print(f"✗ SQLite database file not found: {db_file}")
|
||||
return [], {}
|
||||
|
||||
conn = sqlite3.connect(db_file)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Get list of existing tables
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
|
||||
existing_tables = [row[0] for row in cursor.fetchall()]
|
||||
|
||||
# Get table schemas
|
||||
table_schemas = {}
|
||||
for table in existing_tables:
|
||||
cursor.execute(f"PRAGMA table_info({table})")
|
||||
columns = cursor.fetchall()
|
||||
table_schemas[table] = columns
|
||||
|
||||
conn.close()
|
||||
|
||||
print(f"✓ Found {len(existing_tables)} existing tables: {existing_tables}")
|
||||
return existing_tables, table_schemas
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Error analyzing SQLite schema: {e}")
|
||||
return [], {}
|
||||
|
||||
return [], {}
|
||||
|
||||
def create_migration_strategy(existing_tables, table_schemas):
|
||||
"""Create a migration strategy based on existing schema"""
|
||||
print("\n--- Creating Migration Strategy ---")
|
||||
|
||||
# Define expected tables and their priority
|
||||
expected_tables = [
|
||||
'clients', 'users', 'projects', 'tasks', 'time_entries',
|
||||
'settings', 'invoices', 'invoice_items'
|
||||
]
|
||||
|
||||
missing_tables = [table for table in expected_tables if table not in existing_tables]
|
||||
existing_but_modified = []
|
||||
|
||||
if missing_tables:
|
||||
print(f"Tables to create: {missing_tables}")
|
||||
|
||||
# Check for schema modifications
|
||||
for table in existing_tables:
|
||||
if table in expected_tables:
|
||||
# This table exists, check if it needs modifications
|
||||
print(f"Table '{table}' exists - will preserve data")
|
||||
|
||||
return missing_tables, existing_but_modified
|
||||
|
||||
def initialize_flask_migrate():
|
||||
"""Initialize Flask-Migrate if not already initialized"""
|
||||
migrations_dir = Path("migrations")
|
||||
|
||||
if not migrations_dir.exists():
|
||||
print("Initializing Flask-Migrate...")
|
||||
return run_command("flask db init", "Initialize Flask-Migrate")
|
||||
else:
|
||||
print("✓ Migrations directory already exists")
|
||||
return True
|
||||
|
||||
def create_initial_migration():
|
||||
"""Create the initial migration that captures the current state"""
|
||||
versions_dir = Path("migrations/versions")
|
||||
|
||||
if not versions_dir.exists() or not list(versions_dir.glob("*.py")):
|
||||
print("Creating initial migration...")
|
||||
return run_command("flask db migrate -m 'Initial schema from existing database'", "Create initial migration")
|
||||
else:
|
||||
print("✓ Initial migration already exists")
|
||||
return True
|
||||
|
||||
def stamp_database_with_current_revision():
|
||||
"""Mark the database as being at the current migration revision"""
|
||||
print("Stamping database with current migration revision...")
|
||||
return run_command("flask db stamp head", "Stamp database with current revision")
|
||||
|
||||
def apply_migrations():
|
||||
"""Apply any pending migrations"""
|
||||
return run_command("flask db upgrade", "Apply database migrations")
|
||||
|
||||
def verify_migration_success():
|
||||
"""Verify that the migration was successful"""
|
||||
print("\n--- Verifying Migration Success ---")
|
||||
|
||||
# Check migration status
|
||||
print("Current migration status:")
|
||||
run_command("flask db current", "Show current migration", capture_output=False)
|
||||
|
||||
# Check migration history
|
||||
print("\nMigration history:")
|
||||
run_command("flask db history", "Show migration history", capture_output=False)
|
||||
|
||||
# Test database connection
|
||||
try:
|
||||
from app import create_app, db
|
||||
app = create_app()
|
||||
with app.app_context():
|
||||
# Try to query the database
|
||||
result = db.session.execute(db.text("SELECT 1"))
|
||||
print("✓ Database connection test successful")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"✗ Database connection test failed: {e}")
|
||||
return False
|
||||
|
||||
def create_data_migration_script(existing_tables, table_schemas):
|
||||
"""Create a data migration script for any existing data"""
|
||||
print("\n--- Creating Data Migration Script ---")
|
||||
|
||||
script_content = """#!/usr/bin/env python3
|
||||
\"\"\"
|
||||
Data Migration Script for Existing Database
|
||||
This script handles data migration from old schema to new schema
|
||||
\"\"\"
|
||||
|
||||
from app import create_app, db
|
||||
from app.models import User, Project, TimeEntry, Task, Settings, Invoice, Client
|
||||
|
||||
def migrate_existing_data():
|
||||
\"\"\"Migrate existing data to new schema\"\"\"
|
||||
app = create_app()
|
||||
|
||||
with app.app_context():
|
||||
print("Starting data migration...")
|
||||
|
||||
# Add your data migration logic here
|
||||
# Example: Migrate old client names to new client table
|
||||
|
||||
print("Data migration completed")
|
||||
|
||||
if __name__ == "__main__":
|
||||
migrate_existing_data()
|
||||
"""
|
||||
|
||||
script_path = "migrations/migrate_existing_data.py"
|
||||
with open(script_path, 'w') as f:
|
||||
f.write(script_content)
|
||||
|
||||
print(f"✓ Data migration script created: {script_path}")
|
||||
return script_path
|
||||
|
||||
def main():
|
||||
"""Main migration function"""
|
||||
print("=== TimeTracker Comprehensive Database Migration ===")
|
||||
print("This script will migrate ANY existing database to the new Flask-Migrate system")
|
||||
|
||||
# Check prerequisites
|
||||
if not check_flask_migrate_installed():
|
||||
sys.exit(1)
|
||||
|
||||
# Detect database type
|
||||
db_type, db_url = detect_database_type()
|
||||
if not db_url:
|
||||
print("✗ Could not detect database configuration")
|
||||
print("Please set DATABASE_URL environment variable or ensure database files exist")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"✓ Detected {db_type} database")
|
||||
|
||||
# Create backup
|
||||
print("\n⚠️ IMPORTANT: Creating database backup before proceeding...")
|
||||
backup_file = backup_database(db_type, db_url)
|
||||
if not backup_file:
|
||||
response = input("Failed to create backup. Continue anyway? (y/N): ")
|
||||
if response.lower() != 'y':
|
||||
print("Migration cancelled.")
|
||||
sys.exit(1)
|
||||
|
||||
# Analyze existing schema
|
||||
existing_tables, table_schemas = analyze_existing_schema(db_type, db_url)
|
||||
|
||||
# Create migration strategy
|
||||
missing_tables, modified_tables = create_migration_strategy(existing_tables, table_schemas)
|
||||
|
||||
# Initialize Flask-Migrate
|
||||
if not initialize_flask_migrate():
|
||||
print("Failed to initialize Flask-Migrate")
|
||||
sys.exit(1)
|
||||
|
||||
# Create initial migration
|
||||
if not create_initial_migration():
|
||||
print("Failed to create initial migration")
|
||||
sys.exit(1)
|
||||
|
||||
# Create data migration script if needed
|
||||
if existing_tables:
|
||||
create_data_migration_script(existing_tables, table_schemas)
|
||||
|
||||
# Stamp database with current revision
|
||||
if not stamp_database_with_current_revision():
|
||||
print("Failed to stamp database")
|
||||
sys.exit(1)
|
||||
|
||||
# Apply any pending migrations
|
||||
if not apply_migrations():
|
||||
print("Failed to apply migrations")
|
||||
sys.exit(1)
|
||||
|
||||
# Verify migration success
|
||||
if not verify_migration_success():
|
||||
print("Migration verification failed")
|
||||
sys.exit(1)
|
||||
|
||||
# Show final status
|
||||
print("\n=== Migration Complete ===")
|
||||
print("🎉 Your database has been successfully migrated to Flask-Migrate!")
|
||||
|
||||
print("\nNext steps:")
|
||||
print("1. Test your application to ensure everything works")
|
||||
print("2. Review the generated migration files in migrations/versions/")
|
||||
print("3. If you have existing data, run the data migration script:")
|
||||
print(" python migrations/migrate_existing_data.py")
|
||||
print("4. For future schema changes, use:")
|
||||
print(" - flask db migrate -m 'Description of changes'")
|
||||
print(" - flask db upgrade")
|
||||
|
||||
print("\nBackup created at:", backup_file)
|
||||
print("For more information, see: migrations/README.md")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
24
migrations/script.py.mako
Normal file
@@ -0,0 +1,24 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = ${repr(up_revision)}
|
||||
down_revision = ${repr(down_revision)}
|
||||
branch_labels = ${repr(branch_labels)}
|
||||
depends_on = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade():
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade():
|
||||
${downgrades if downgrades else "pass"}
|
||||
161
migrations/test_migration_system.py
Normal file
@@ -0,0 +1,161 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test Script for TimeTracker Migration System
|
||||
This script verifies that the migration system is working correctly
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
def test_flask_migrate_installation():
|
||||
"""Test if Flask-Migrate is properly installed"""
|
||||
try:
|
||||
import flask_migrate
|
||||
print("✓ Flask-Migrate is installed")
|
||||
return True
|
||||
except ImportError:
|
||||
print("✗ Flask-Migrate is not installed")
|
||||
return False
|
||||
|
||||
def test_database_connection():
|
||||
"""Test database connection"""
|
||||
try:
|
||||
from app import create_app, db
|
||||
app = create_app()
|
||||
|
||||
with app.app_context():
|
||||
# Test basic connection
|
||||
result = db.session.execute(db.text("SELECT 1"))
|
||||
print("✓ Database connection successful")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"✗ Database connection failed: {e}")
|
||||
return False
|
||||
|
||||
def test_migration_files():
|
||||
"""Test if migration files exist and are valid"""
|
||||
migrations_dir = Path("migrations")
|
||||
|
||||
if not migrations_dir.exists():
|
||||
print("✗ Migrations directory does not exist")
|
||||
return False
|
||||
|
||||
required_files = [
|
||||
"env.py",
|
||||
"script.py.mako",
|
||||
"alembic.ini",
|
||||
"README.md"
|
||||
]
|
||||
|
||||
for file in required_files:
|
||||
if not (migrations_dir / file).exists():
|
||||
print(f"✗ Required file missing: {file}")
|
||||
return False
|
||||
|
||||
print("✓ All required migration files exist")
|
||||
return True
|
||||
|
||||
def test_migration_commands():
|
||||
"""Test if migration commands are available"""
|
||||
try:
|
||||
# Test flask db init (should not fail if already initialized)
|
||||
result = os.system("flask db --help > /dev/null 2>&1")
|
||||
if result == 0:
|
||||
print("✓ Flask migration commands available")
|
||||
return True
|
||||
else:
|
||||
print("✗ Flask migration commands not available")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"✗ Error testing migration commands: {e}")
|
||||
return False
|
||||
|
||||
def test_models_import():
|
||||
"""Test if all models can be imported"""
|
||||
try:
|
||||
from app.models import User, Project, TimeEntry, Task, Settings, Invoice, Client
|
||||
print("✓ All models imported successfully")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"✗ Error importing models: {e}")
|
||||
return False
|
||||
|
||||
def test_migration_scripts():
|
||||
"""Test if migration scripts exist and are valid"""
|
||||
scripts = [
|
||||
"migrate_existing_database.py",
|
||||
"legacy_schema_migration.py",
|
||||
"manage_migrations.py"
|
||||
]
|
||||
|
||||
for script in scripts:
|
||||
script_path = Path("migrations") / script
|
||||
if not script_path.exists():
|
||||
print(f"✗ Migration script missing: {script}")
|
||||
return False
|
||||
|
||||
print("✓ All migration scripts exist")
|
||||
return True
|
||||
|
||||
def run_comprehensive_test():
|
||||
"""Run all tests"""
|
||||
print("=== Testing TimeTracker Migration System ===\n")
|
||||
|
||||
tests = [
|
||||
("Flask-Migrate Installation", test_flask_migrate_installation),
|
||||
("Database Connection", test_database_connection),
|
||||
("Migration Files", test_migration_files),
|
||||
("Migration Commands", test_migration_commands),
|
||||
("Models Import", test_models_import),
|
||||
("Migration Scripts", test_migration_scripts)
|
||||
]
|
||||
|
||||
passed = 0
|
||||
total = len(tests)
|
||||
|
||||
for test_name, test_func in tests:
|
||||
print(f"Testing: {test_name}")
|
||||
if test_func():
|
||||
passed += 1
|
||||
print()
|
||||
|
||||
print(f"=== Test Results ===")
|
||||
print(f"Passed: {passed}/{total}")
|
||||
|
||||
if passed == total:
|
||||
print("🎉 All tests passed! Migration system is ready.")
|
||||
return True
|
||||
else:
|
||||
print("⚠️ Some tests failed. Please fix issues before proceeding.")
|
||||
return False
|
||||
|
||||
def main():
|
||||
"""Main function"""
|
||||
if not Path("app.py").exists():
|
||||
print("Error: Please run this script from the TimeTracker root directory")
|
||||
sys.exit(1)
|
||||
|
||||
# Set environment variables
|
||||
os.environ.setdefault('FLASK_APP', 'app.py')
|
||||
|
||||
success = run_comprehensive_test()
|
||||
|
||||
if success:
|
||||
print("\n✅ Migration system is ready for use!")
|
||||
print("\nNext steps:")
|
||||
print("1. For fresh installation: python migrations/manage_migrations.py")
|
||||
print("2. For existing database: python migrations/migrate_existing_database.py")
|
||||
print("3. For legacy schema: python migrations/legacy_schema_migration.py")
|
||||
else:
|
||||
print("\n❌ Migration system has issues that need to be resolved.")
|
||||
print("\nTroubleshooting:")
|
||||
print("1. Install Flask-Migrate: pip install Flask-Migrate")
|
||||
print("2. Check database connection settings")
|
||||
print("3. Verify all required files exist")
|
||||
print("4. Check application configuration")
|
||||
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
187
migrations/versions/001_initial_schema.py
Normal file
@@ -0,0 +1,187 @@
|
||||
"""Initial database schema
|
||||
|
||||
Revision ID: 001
|
||||
Revises:
|
||||
Create Date: 2025-01-15 10:00:00.000000
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '001'
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# Create clients table
|
||||
op.create_table('clients',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=200), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('contact_person', sa.String(length=200), nullable=True),
|
||||
sa.Column('email', sa.String(length=200), nullable=True),
|
||||
sa.Column('phone', sa.String(length=50), nullable=True),
|
||||
sa.Column('address', sa.Text(), nullable=True),
|
||||
sa.Column('default_hourly_rate', sa.Numeric(precision=9, scale=2), nullable=True),
|
||||
sa.Column('status', sa.String(length=20), nullable=False, server_default='active'),
|
||||
sa.Column('created_at', sa.TIMESTAMP(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')),
|
||||
sa.Column('updated_at', sa.TIMESTAMP(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index('idx_clients_name', 'clients', ['name'], unique=True)
|
||||
|
||||
# Create users table
|
||||
op.create_table('users',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('username', sa.String(length=80), nullable=False),
|
||||
sa.Column('role', sa.String(length=20), nullable=False, server_default='user'),
|
||||
sa.Column('created_at', sa.TIMESTAMP(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')),
|
||||
sa.Column('last_login', sa.TIMESTAMP(), nullable=True),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=False, server_default='true'),
|
||||
sa.Column('updated_at', sa.TIMESTAMP(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index('idx_users_username', 'users', ['username'], unique=True)
|
||||
op.create_index('idx_users_role', 'users', ['role'])
|
||||
|
||||
# Create projects table
|
||||
op.create_table('projects',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=200), nullable=False),
|
||||
sa.Column('client_id', sa.Integer(), nullable=True),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('billable', sa.Boolean(), nullable=False, server_default='true'),
|
||||
sa.Column('hourly_rate', sa.Numeric(precision=9, scale=2), nullable=True),
|
||||
sa.Column('billing_ref', sa.String(length=100), nullable=True),
|
||||
sa.Column('status', sa.String(length=20), nullable=False, server_default='active'),
|
||||
sa.Column('created_at', sa.TIMESTAMP(), nullable=True, server_default=sa.text('CURRENT_TIMESTAMP')),
|
||||
sa.Column('updated_at', sa.TIMESTAMP(), nullable=True, server_default=sa.text('CURRENT_TIMESTAMP')),
|
||||
sa.ForeignKeyConstraint(['client_id'], ['clients.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index('idx_projects_client_id', 'projects', ['client_id'])
|
||||
op.create_index('idx_projects_status', 'projects', ['status'])
|
||||
|
||||
# Create tasks table
|
||||
op.create_table('tasks',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('project_id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=200), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('status', sa.String(length=20), nullable=False, server_default='pending'),
|
||||
sa.Column('priority', sa.String(length=20), nullable=False, server_default='medium'),
|
||||
sa.Column('assigned_to', sa.Integer(), nullable=True),
|
||||
sa.Column('created_by', sa.Integer(), nullable=False),
|
||||
sa.Column('due_date', sa.Date(), nullable=True),
|
||||
sa.Column('estimated_hours', sa.Numeric(precision=5, scale=2), nullable=True),
|
||||
sa.Column('created_at', sa.TIMESTAMP(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')),
|
||||
sa.Column('updated_at', sa.TIMESTAMP(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')),
|
||||
sa.ForeignKeyConstraint(['assigned_to'], ['users.id'], ondelete='SET NULL'),
|
||||
sa.ForeignKeyConstraint(['created_by'], ['users.id'], ondelete='CASCADE'),
|
||||
sa.ForeignKeyConstraint(['project_id'], ['projects.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index('idx_tasks_project_id', 'tasks', ['project_id'])
|
||||
op.create_index('idx_tasks_status', 'tasks', ['status'])
|
||||
op.create_index('idx_tasks_priority', 'tasks', ['priority'])
|
||||
|
||||
# Create time_entries table
|
||||
op.create_table('time_entries',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||
sa.Column('project_id', sa.Integer(), nullable=False),
|
||||
sa.Column('task_id', sa.Integer(), nullable=True),
|
||||
sa.Column('start_time', sa.TIMESTAMP(), nullable=False),
|
||||
sa.Column('end_time', sa.TIMESTAMP(), nullable=True),
|
||||
sa.Column('duration', sa.Interval(), nullable=True),
|
||||
sa.Column('notes', sa.Text(), nullable=True),
|
||||
sa.Column('tags', sa.String(length=500), nullable=True),
|
||||
sa.Column('billable', sa.Boolean(), nullable=False, server_default='true'),
|
||||
sa.Column('source', sa.String(length=50), nullable=True),
|
||||
sa.Column('created_at', sa.TIMESTAMP(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')),
|
||||
sa.Column('updated_at', sa.TIMESTAMP(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')),
|
||||
sa.ForeignKeyConstraint(['project_id'], ['projects.id'], ondelete='CASCADE'),
|
||||
sa.ForeignKeyConstraint(['task_id'], ['tasks.id'], ondelete='SET NULL'),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index('idx_time_entries_user_id', 'time_entries', ['user_id'])
|
||||
op.create_index('idx_time_entries_project_id', 'time_entries', ['project_id'])
|
||||
op.create_index('idx_time_entries_start_time', 'time_entries', ['start_time'])
|
||||
|
||||
# Create settings table
|
||||
op.create_table('settings',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('company_name', sa.String(length=200), nullable=True),
|
||||
sa.Column('company_address', sa.Text(), nullable=True),
|
||||
sa.Column('company_phone', sa.String(length=50), nullable=True),
|
||||
sa.Column('company_email', sa.String(length=200), nullable=True),
|
||||
sa.Column('company_website', sa.String(length=200), nullable=True),
|
||||
sa.Column('currency', sa.String(length=3), nullable=False, server_default='EUR'),
|
||||
sa.Column('timezone', sa.String(length=50), nullable=False, server_default='Europe/Rome'),
|
||||
sa.Column('rounding_minutes', sa.Integer(), nullable=False, server_default='1'),
|
||||
sa.Column('single_active_timer', sa.Boolean(), nullable=False, server_default='true'),
|
||||
sa.Column('idle_timeout_minutes', sa.Integer(), nullable=False, server_default='30'),
|
||||
sa.Column('allow_self_register', sa.Boolean(), nullable=False, server_default='true'),
|
||||
sa.Column('allow_analytics', sa.Boolean(), nullable=False, server_default='true'),
|
||||
sa.Column('logo_path', sa.String(length=500), nullable=True),
|
||||
sa.Column('created_at', sa.TIMESTAMP(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')),
|
||||
sa.Column('updated_at', sa.TIMESTAMP(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
|
||||
# Create invoices table
|
||||
op.create_table('invoices',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('invoice_number', sa.String(length=50), nullable=False),
|
||||
sa.Column('client_id', sa.Integer(), nullable=False),
|
||||
sa.Column('project_id', sa.Integer(), nullable=True),
|
||||
sa.Column('issue_date', sa.Date(), nullable=False),
|
||||
sa.Column('due_date', sa.Date(), nullable=False),
|
||||
sa.Column('status', sa.String(length=20), nullable=False, server_default='draft'),
|
||||
sa.Column('subtotal', sa.Numeric(precision=10, scale=2), nullable=False, server_default='0'),
|
||||
sa.Column('tax_rate', sa.Numeric(precision=5, scale=2), nullable=False, server_default='0'),
|
||||
sa.Column('tax_amount', sa.Numeric(precision=10, scale=2), nullable=False, server_default='0'),
|
||||
sa.Column('total', sa.Numeric(precision=10, scale=2), nullable=False, server_default='0'),
|
||||
sa.Column('notes', sa.Text(), nullable=True),
|
||||
sa.Column('terms', sa.Text(), nullable=True),
|
||||
sa.Column('created_at', sa.TIMESTAMP(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')),
|
||||
sa.Column('updated_at', sa.TIMESTAMP(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')),
|
||||
sa.ForeignKeyConstraint(['client_id'], ['clients.id'], ondelete='CASCADE'),
|
||||
sa.ForeignKeyConstraint(['project_id'], ['projects.id'], ondelete='SET NULL'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index('idx_invoices_client_id', 'invoices', ['client_id'])
|
||||
op.create_index('idx_invoices_status', 'invoices', ['status'])
|
||||
|
||||
# Create invoice_items table
|
||||
op.create_table('invoice_items',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('invoice_id', sa.Integer(), nullable=False),
|
||||
sa.Column('description', sa.String(length=500), nullable=False),
|
||||
sa.Column('quantity', sa.Numeric(precision=10, scale=2), nullable=False, server_default='1'),
|
||||
sa.Column('unit_price', sa.Numeric(precision=9, scale=2), nullable=False, server_default='0'),
|
||||
sa.Column('total', sa.Numeric(precision=10, scale=2), nullable=False, server_default='0'),
|
||||
sa.Column('time_entry_id', sa.Integer(), nullable=True),
|
||||
sa.Column('created_at', sa.TIMESTAMP(), nullable=False, server_default=sa.text('CURRENT_TIMESTAMP')),
|
||||
sa.ForeignKeyConstraint(['invoice_id'], ['invoices.id'], ondelete='CASCADE'),
|
||||
sa.ForeignKeyConstraint(['time_entry_id'], ['time_entries.id'], ondelete='SET NULL'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index('idx_invoice_items_invoice_id', 'invoice_items', ['invoice_id'])
|
||||
|
||||
|
||||
def downgrade():
|
||||
# Drop tables in reverse order
|
||||
op.drop_table('invoice_items')
|
||||
op.drop_table('invoices')
|
||||
op.drop_table('settings')
|
||||
op.drop_table('time_entries')
|
||||
op.drop_table('tasks')
|
||||
op.drop_table('projects')
|
||||
op.drop_table('users')
|
||||
op.drop_table('clients')
|
||||
166
scripts/setup-migrations.bat
Normal file
@@ -0,0 +1,166 @@
|
||||
@echo off
|
||||
REM TimeTracker Migration Setup Script for Windows
|
||||
REM This script helps set up Flask-Migrate for database migrations
|
||||
|
||||
echo === TimeTracker Migration Setup ===
|
||||
echo This script will help you set up Flask-Migrate for database migrations
|
||||
echo.
|
||||
|
||||
REM Check if we're in the right directory
|
||||
if not exist "app.py" (
|
||||
echo Error: Please run this script from the TimeTracker root directory
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Check if Python is available
|
||||
python --version >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo Error: Python is required but not installed or not in PATH
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Check if Flask is available
|
||||
python -c "import flask" >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo Error: Flask is required but not installed
|
||||
echo Please install dependencies with: pip install -r requirements.txt
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Check if Flask-Migrate is available
|
||||
python -c "import flask_migrate" >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo Error: Flask-Migrate is required but not installed
|
||||
echo Please install dependencies with: pip install -r requirements.txt
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo ✓ Prerequisites check passed
|
||||
echo.
|
||||
|
||||
REM Set environment variables if not already set
|
||||
if "%FLASK_APP%"=="" (
|
||||
set FLASK_APP=app.py
|
||||
echo Set FLASK_APP=app.py
|
||||
)
|
||||
|
||||
REM Check if migrations directory exists
|
||||
if exist "migrations" (
|
||||
echo ✓ Migrations directory already exists
|
||||
|
||||
REM Check if it's properly initialized
|
||||
if exist "migrations\env.py" if exist "migrations\alembic.ini" (
|
||||
echo ✓ Flask-Migrate is already initialized
|
||||
|
||||
REM Show current status
|
||||
echo.
|
||||
echo Current migration status:
|
||||
flask db current 2>nul || echo No migrations applied yet
|
||||
|
||||
echo.
|
||||
echo Migration history:
|
||||
flask db history 2>nul || echo No migration history
|
||||
|
||||
echo.
|
||||
echo To create a new migration:
|
||||
echo flask db migrate -m "Description of changes"
|
||||
echo.
|
||||
echo To apply pending migrations:
|
||||
echo flask db upgrade
|
||||
|
||||
pause
|
||||
exit /b 0
|
||||
) else (
|
||||
echo ⚠ Migrations directory exists but appears incomplete
|
||||
echo Removing and reinitializing...
|
||||
rmdir /s /q migrations
|
||||
)
|
||||
)
|
||||
|
||||
REM Initialize Flask-Migrate
|
||||
echo Initializing Flask-Migrate...
|
||||
flask db init
|
||||
|
||||
if errorlevel 1 (
|
||||
echo ✗ Failed to initialize Flask-Migrate
|
||||
pause
|
||||
exit /b 1
|
||||
) else (
|
||||
echo ✓ Flask-Migrate initialized successfully
|
||||
)
|
||||
|
||||
REM Create initial migration
|
||||
echo.
|
||||
echo Creating initial migration...
|
||||
flask db migrate -m "Initial database schema"
|
||||
|
||||
if errorlevel 1 (
|
||||
echo ✗ Failed to create initial migration
|
||||
pause
|
||||
exit /b 1
|
||||
) else (
|
||||
echo ✓ Initial migration created successfully
|
||||
)
|
||||
|
||||
REM Show the generated migration
|
||||
echo.
|
||||
echo Generated migration file:
|
||||
dir migrations\versions
|
||||
|
||||
echo.
|
||||
echo Review the migration file before applying:
|
||||
echo type migrations\versions\*.py
|
||||
|
||||
REM Ask user if they want to apply the migration
|
||||
echo.
|
||||
set /p "apply_migration=Do you want to apply this migration now? (y/N): "
|
||||
|
||||
if /i "%apply_migration%"=="y" (
|
||||
echo Applying migration...
|
||||
flask db upgrade
|
||||
|
||||
if errorlevel 1 (
|
||||
echo ✗ Failed to apply migration
|
||||
pause
|
||||
exit /b 1
|
||||
) else (
|
||||
echo ✓ Migration applied successfully
|
||||
|
||||
echo.
|
||||
echo Current migration status:
|
||||
flask db current
|
||||
|
||||
echo.
|
||||
echo Migration history:
|
||||
flask db history
|
||||
)
|
||||
) else (
|
||||
echo Migration not applied. You can apply it later with:
|
||||
echo flask db upgrade
|
||||
)
|
||||
|
||||
echo.
|
||||
echo === Setup Complete ===
|
||||
echo.
|
||||
echo Your Flask-Migrate system is now set up!
|
||||
echo.
|
||||
echo Next steps:
|
||||
echo 1. Test your application to ensure everything works
|
||||
echo 2. For future schema changes:
|
||||
echo - Edit your models in app\models\
|
||||
echo - Run: flask db migrate -m "Description of changes"
|
||||
echo - Review the generated migration file
|
||||
echo - Run: flask db upgrade
|
||||
echo.
|
||||
echo Useful commands:
|
||||
echo flask db current # Show current migration
|
||||
echo flask db history # Show migration history
|
||||
echo flask db downgrade # Rollback last migration
|
||||
echo.
|
||||
echo For more information, see: migrations\README.md
|
||||
|
||||
pause
|
||||
157
scripts/setup-migrations.sh
Normal file
@@ -0,0 +1,157 @@
|
||||
#!/bin/bash
|
||||
|
||||
# TimeTracker Migration Setup Script
|
||||
# This script helps set up Flask-Migrate for database migrations
|
||||
|
||||
set -e
|
||||
|
||||
echo "=== TimeTracker Migration Setup ==="
|
||||
echo "This script will help you set up Flask-Migrate for database migrations"
|
||||
echo ""
|
||||
|
||||
# Check if we're in the right directory
|
||||
if [ ! -f "app.py" ]; then
|
||||
echo "Error: Please run this script from the TimeTracker root directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if Python is available
|
||||
if ! command -v python3 &> /dev/null; then
|
||||
echo "Error: Python 3 is required but not installed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if Flask is available
|
||||
if ! python3 -c "import flask" &> /dev/null; then
|
||||
echo "Error: Flask is required but not installed"
|
||||
echo "Please install dependencies with: pip install -r requirements.txt"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if Flask-Migrate is available
|
||||
if ! python3 -c "import flask_migrate" &> /dev/null; then
|
||||
echo "Error: Flask-Migrate is required but not installed"
|
||||
echo "Please install dependencies with: pip install -r requirements.txt"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✓ Prerequisites check passed"
|
||||
echo ""
|
||||
|
||||
# Set environment variables if not already set
|
||||
if [ -z "$FLASK_APP" ]; then
|
||||
export FLASK_APP=app.py
|
||||
echo "Set FLASK_APP=app.py"
|
||||
fi
|
||||
|
||||
# Check if migrations directory exists
|
||||
if [ -d "migrations" ]; then
|
||||
echo "✓ Migrations directory already exists"
|
||||
|
||||
# Check if it's properly initialized
|
||||
if [ -f "migrations/env.py" ] && [ -f "migrations/alembic.ini" ]; then
|
||||
echo "✓ Flask-Migrate is already initialized"
|
||||
|
||||
# Show current status
|
||||
echo ""
|
||||
echo "Current migration status:"
|
||||
flask db current || echo "No migrations applied yet"
|
||||
|
||||
echo ""
|
||||
echo "Migration history:"
|
||||
flask db history || echo "No migration history"
|
||||
|
||||
echo ""
|
||||
echo "To create a new migration:"
|
||||
echo " flask db migrate -m 'Description of changes'"
|
||||
echo ""
|
||||
echo "To apply pending migrations:"
|
||||
echo " flask db upgrade"
|
||||
|
||||
exit 0
|
||||
else
|
||||
echo "⚠ Migrations directory exists but appears incomplete"
|
||||
echo "Removing and reinitializing..."
|
||||
rm -rf migrations
|
||||
fi
|
||||
fi
|
||||
|
||||
# Initialize Flask-Migrate
|
||||
echo "Initializing Flask-Migrate..."
|
||||
flask db init
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "✓ Flask-Migrate initialized successfully"
|
||||
else
|
||||
echo "✗ Failed to initialize Flask-Migrate"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create initial migration
|
||||
echo ""
|
||||
echo "Creating initial migration..."
|
||||
flask db migrate -m "Initial database schema"
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "✓ Initial migration created successfully"
|
||||
else
|
||||
echo "✗ Failed to create initial migration"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Show the generated migration
|
||||
echo ""
|
||||
echo "Generated migration file:"
|
||||
ls -la migrations/versions/
|
||||
|
||||
echo ""
|
||||
echo "Review the migration file before applying:"
|
||||
echo " cat migrations/versions/*.py"
|
||||
|
||||
# Ask user if they want to apply the migration
|
||||
echo ""
|
||||
read -p "Do you want to apply this migration now? (y/N): " -n 1 -r
|
||||
echo ""
|
||||
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "Applying migration..."
|
||||
flask db upgrade
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "✓ Migration applied successfully"
|
||||
|
||||
echo ""
|
||||
echo "Current migration status:"
|
||||
flask db current
|
||||
|
||||
echo ""
|
||||
echo "Migration history:"
|
||||
flask db history
|
||||
else
|
||||
echo "✗ Failed to apply migration"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "Migration not applied. You can apply it later with:"
|
||||
echo " flask db upgrade"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Setup Complete ==="
|
||||
echo ""
|
||||
echo "Your Flask-Migrate system is now set up!"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo "1. Test your application to ensure everything works"
|
||||
echo "2. For future schema changes:"
|
||||
echo " - Edit your models in app/models/"
|
||||
echo " - Run: flask db migrate -m 'Description of changes'"
|
||||
echo " - Review the generated migration file"
|
||||
echo " - Run: flask db upgrade"
|
||||
echo ""
|
||||
echo "Useful commands:"
|
||||
echo " flask db current # Show current migration"
|
||||
echo " flask db history # Show migration history"
|
||||
echo " flask db downgrade # Rollback last migration"
|
||||
echo ""
|
||||
echo "For more information, see: migrations/README.md"
|
||||