6 Commits

Author SHA1 Message Date
ryan
e20e5eac01 Merge pull request 'asdf' (#5) from add-claude-migration-guide into main
Reviewed-on: #5
2025-08-15 07:49:15 -04:00
Ryan Chen
e9803eff37 asdf 2025-08-15 07:49:05 -04:00
ryan
41123f046e Merge pull request 'docs: add Claude development guidelines for database migrations' (#4) from add-claude-migration-guide into main
Reviewed-on: #4
2025-08-08 00:13:28 -04:00
ryan
013c398296 Merge pull request 'feat: add comprehensive database migration system' (#3) from add-migrations-system into main
Reviewed-on: #3
2025-08-08 00:13:21 -04:00
Ryan Chen
6c4acf438d docs: add Claude development guidelines for database migrations
- Create CLAUDE.md with comprehensive migration guidelines
- Emphasize MUST create migrations for all schema changes
- Provide complete migration template and workflow
- Include naming conventions and best practices
- Document current database schema reference
- Add integration examples for dev/Docker/production

This ensures future AI assistants and developers follow
proper migration practices to prevent schema inconsistencies.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-08 00:07:23 -04:00
Ryan Chen
bef2577e13 feat: add comprehensive database migration system
- Create migrations/ directory with proper structure
- Add migration runner (migrations/migrate.py) with tracking
- Implement 001_add_description_column.py migration
- Add comprehensive README with usage and best practices
- Support for ordered execution and rollback capabilities
- Integration ready for Docker and CI/CD workflows

The migration system provides:
- Ordered execution by filename (001_, 002_, etc.)
- Applied migration tracking in database
- Idempotent operations (safe to re-run)
- Standalone and batch execution modes
- Comprehensive error handling and logging

Restored description column that was missing after merge.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-08 00:05:37 -04:00
6 changed files with 610 additions and 0 deletions

201
CLAUDE.md Normal file
View File

@@ -0,0 +1,201 @@
# Claude Development Guidelines
## Database Migrations
**⚠️ IMPORTANT: When making changes to database models or schema, you MUST create migration files.**
### When to Create Migrations
**ALWAYS create a migration when:**
- Adding new columns to existing tables
- Creating new tables
- Modifying column types or constraints
- Adding or removing indexes
- Changing default values
- Adding foreign keys or relationships
**Examples that require migrations:**
```python
# Adding a new field to PetPicture model
class PetPicture:
# ... existing fields ...
tags = models.TextField() # ❌ Requires migration!
```
```python
# Changing database schema in init_db()
def init_db():
db.execute("""
CREATE TABLE users ( -- ❌ New table requires migration!
id INTEGER PRIMARY KEY,
username TEXT NOT NULL
)
""")
```
### How to Create Migrations
1. **Navigate to migrations directory:**
```bash
cd migrations/
```
2. **Create new migration file:**
```bash
# Use next sequential number (002, 003, etc.)
touch XXX_descriptive_name.py
```
3. **Use the migration template:**
```python
#!/usr/bin/env python3
"""
Migration XXX: Brief description of changes
Date: YYYY-MM-DD
Description: Detailed explanation of what this migration does
"""
import sqlite3
import sys
from datetime import datetime
def check_column_exists(cursor, table_name, column_name):
"""Check if a column exists in a table"""
cursor.execute(f"PRAGMA table_info({table_name})")
columns = [column[1] for column in cursor.fetchall()]
return column_name in columns
def migrate_up(cursor):
"""Apply the migration"""
# Check if already applied
if check_column_exists(cursor, 'pet_pictures', 'new_column'):
print("Migration already applied, skipping...")
return False
# Apply the migration
print("Adding new_column to pet_pictures table...")
cursor.execute("ALTER TABLE pet_pictures ADD COLUMN new_column TEXT")
return True
def migrate_down(cursor):
"""Rollback the migration (optional)"""
print("Rollback not implemented for SQLite column drops")
return False
def main():
"""Run migration standalone"""
# Standard migration execution code
pass
if __name__ == "__main__":
sys.exit(main())
```
4. **Test the migration:**
```bash
# Test individual migration
python migrations/XXX_descriptive_name.py
# Or run all pending migrations
python migrations/migrate.py
```
5. **Commit the migration file:**
```bash
git add migrations/XXX_descriptive_name.py
git commit -m "feat: add migration for [description]"
```
### Migration Naming Convention
- **Sequential numbering**: `001_`, `002_`, `003_`, etc.
- **Descriptive names**: Explain what the migration does
- **Examples**:
- `002_add_user_table.py`
- `003_add_tags_column_to_pictures.py`
- `004_create_comments_table.py`
### DO NOT Modify Database Schema Without Migrations
**❌ NEVER do this:**
```python
# Directly modifying init_db() or model schema
def init_db():
db.execute("""
CREATE TABLE pet_pictures (
id INTEGER PRIMARY KEY,
filename TEXT NOT NULL,
new_field TEXT -- ❌ Added without migration!
)
""")
```
**✅ ALWAYS do this:**
1. Create migration file first
2. Run migration to update schema
3. Then update model code if needed
### Model Changes Workflow
1. **Identify needed schema change**
2. **Create migration file**
3. **Test migration locally**
4. **Update model/application code** (if needed)
5. **Test full application**
6. **Commit migration + code changes together**
### Migration Best Practices
- **One logical change per migration**
- **Make migrations idempotent** (safe to run multiple times)
- **Test on development data first**
- **Include rollback logic when possible**
- **Document breaking changes clearly**
- **Never edit applied migrations**
### Integration Points
**Development:**
```bash
# Apply migrations before starting app
python migrations/migrate.py && python main.py
```
**Docker:**
```bash
# Migrations run automatically via docker-compose
docker compose --profile migrate up migrate
```
**Production Deployment:**
```bash
# Run migrations as part of deployment
python migrations/migrate.py
systemctl restart pet-picture-queue
```
---
## Database Schema Reference
**Current Schema (after migrations):**
### `pet_pictures` table:
- `id` - INTEGER PRIMARY KEY AUTOINCREMENT
- `filename` - TEXT NOT NULL
- `subscriber_name` - TEXT NOT NULL
- `description` - TEXT NULL (added via migration 001)
- `uploaded_at` - TIMESTAMP NOT NULL
- `posted` - BOOLEAN DEFAULT 0
- `likes` - INTEGER DEFAULT 0
### `migrations` table (auto-created):
- `id` - INTEGER PRIMARY KEY AUTOINCREMENT
- `migration_name` - TEXT NOT NULL UNIQUE
- `applied_at` - TIMESTAMP NOT NULL
---
**Remember: Schema changes without migrations will cause deployment issues and data inconsistencies!**
Always follow the migration workflow for database changes.

View File

@@ -1 +1,13 @@
2025-08-07 18:12:42,230 INFO: Pets of Powerwashing startup [in /Users/ryanchen/Programs/pet-picture-queue/app/utils/logging_config.py:29] 2025-08-07 18:12:42,230 INFO: Pets of Powerwashing startup [in /Users/ryanchen/Programs/pet-picture-queue/app/utils/logging_config.py:29]
2025-08-07 23:46:18,045 INFO: Pets of Powerwashing startup [in /Users/ryanchen/Programs/pet-picture-queue/app/utils/logging_config.py:29]
2025-08-07 23:46:25,378 INFO: Pets of Powerwashing startup [in /Users/ryanchen/Programs/pet-picture-queue/app/utils/logging_config.py:29]
2025-08-07 23:46:48,136 INFO: Pets of Powerwashing startup [in /Users/ryanchen/Programs/pet-picture-queue/app/utils/logging_config.py:29]
2025-08-07 23:46:56,323 INFO: Pets of Powerwashing startup [in /Users/ryanchen/Programs/pet-picture-queue/app/utils/logging_config.py:29]
2025-08-07 23:47:05,316 INFO: Pets of Powerwashing startup [in /Users/ryanchen/Programs/pet-picture-queue/app/utils/logging_config.py:29]
2025-08-07 23:47:20,820 INFO: Pets of Powerwashing startup [in /Users/ryanchen/Programs/pet-picture-queue/app/utils/logging_config.py:29]
2025-08-07 23:47:44,082 INFO: Pets of Powerwashing startup [in /Users/ryanchen/Programs/pet-picture-queue/app/utils/logging_config.py:29]
2025-08-07 23:47:44,243 INFO: Pets of Powerwashing startup [in /Users/ryanchen/Programs/pet-picture-queue/app/utils/logging_config.py:29]
2025-08-07 23:48:09,264 ERROR: 404 error: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again. [in /Users/ryanchen/Programs/pet-picture-queue/app/utils/error_handlers.py:13]
2025-08-15 07:36:43,456 INFO: Pets of Powerwashing startup [in /Users/ryanchen/Programs/pet-picture-queue/app/utils/logging_config.py:29]
2025-08-15 07:36:47,429 ERROR: 404 error: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again. [in /Users/ryanchen/Programs/pet-picture-queue/app/utils/error_handlers.py:13]
2025-08-15 07:37:31,681 INFO: Pets of Powerwashing startup [in /Users/ryanchen/Programs/pet-picture-queue/app/utils/logging_config.py:29]

View File

@@ -0,0 +1,80 @@
#!/usr/bin/env python3
"""
Migration 001: Add description column
Date: 2025-08-08
Description: Add description field to pet_pictures table for optional image descriptions
"""
import sqlite3
import sys
from datetime import datetime
def check_column_exists(cursor, table_name, column_name):
"""Check if a column exists in a table"""
cursor.execute(f"PRAGMA table_info({table_name})")
columns = [column[1] for column in cursor.fetchall()]
return column_name in columns
def migrate_up(cursor):
"""Apply the migration"""
if not check_column_exists(cursor, 'pet_pictures', 'description'):
print("Adding description column to pet_pictures table...")
cursor.execute("ALTER TABLE pet_pictures ADD COLUMN description TEXT")
return True
else:
print("Description column already exists, skipping...")
return False
def migrate_down(cursor):
"""Rollback the migration (SQLite doesn't support dropping columns easily)"""
print("WARNING: Cannot rollback this migration easily in SQLite")
print("Description column will remain but can be ignored")
return False
def main():
"""Run the migration"""
db_path = "pet_pictures.db"
try:
db = sqlite3.connect(db_path)
cursor = db.cursor()
print(f"Running migration 001: Add description column")
print(f"Database: {db_path}")
print(f"Timestamp: {datetime.now().isoformat()}")
print("-" * 50)
if migrate_up(cursor):
db.commit()
print("✓ Migration 001 applied successfully")
else:
print("• Migration 001 already applied")
# Verify the migration
cursor.execute("PRAGMA table_info(pet_pictures)")
columns = cursor.fetchall()
print("\nFinal schema:")
for col in columns:
nullable = "NOT NULL" if col[3] else "NULL"
default = f"DEFAULT {col[4]}" if col[4] else ""
print(f" {col[1]} {col[2]} {nullable} {default}")
except sqlite3.Error as e:
print(f"❌ Migration failed: {e}")
return 1
except Exception as e:
print(f"❌ Unexpected error: {e}")
return 1
finally:
if 'db' in locals():
db.close()
return 0
if __name__ == "__main__":
sys.exit(main())

142
migrations/README.md Normal file
View File

@@ -0,0 +1,142 @@
# Database Migrations
This directory contains database migrations for the Pet Picture Queue application.
## Migration System
The migration system provides:
-**Ordered execution** - Migrations run in filename order (001_, 002_, etc.)
-**Tracking** - Applied migrations are tracked in the database
-**Idempotent** - Safe to run multiple times
-**Rollback support** - Individual migrations can implement rollback logic
## Usage
### Run All Pending Migrations
```bash
# From project root
python migrations/migrate.py
# Or make it executable and run directly
chmod +x migrations/migrate.py
./migrations/migrate.py
```
### Run Individual Migration
```bash
python migrations/001_add_description_column.py
```
## Migration Structure
Each migration file should follow this pattern:
```python
#!/usr/bin/env python3
"""
Migration XXX: Description of what this migration does
Date: YYYY-MM-DD
Description: Detailed explanation
"""
import sqlite3
import sys
from datetime import datetime
def migrate_up(cursor):
"""Apply the migration"""
# Your migration code here
cursor.execute("ALTER TABLE ...")
return True # or False if skipped
def migrate_down(cursor):
"""Rollback the migration (optional)"""
# Rollback code here
return True
def main():
"""Run migration standalone"""
# Standard standalone execution code
pass
if __name__ == "__main__":
sys.exit(main())
```
## Existing Migrations
### 001_add_description_column.py
- **Purpose**: Add description field to pet_pictures table
- **Date**: 2025-08-08
- **Changes**: Adds `description TEXT NULL` column
## Migration Naming Convention
- Use 3-digit numbers: `001_`, `002_`, `003_`, etc.
- Descriptive names: `add_description_column`, `create_user_table`
- Full format: `XXX_descriptive_name.py`
## Database Schema Tracking
The migration system creates a `migrations` table to track applied migrations:
```sql
CREATE TABLE migrations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
migration_name TEXT NOT NULL UNIQUE,
applied_at TIMESTAMP NOT NULL
);
```
## Best Practices
1. **Always backup** before running migrations on production data
2. **Test migrations** on development data first
3. **Make migrations idempotent** - check if changes already exist
4. **Keep migrations small** - one logical change per migration
5. **Never edit** existing migration files after they've been applied
6. **Document breaking changes** in migration comments
## Development Workflow
1. **Create migration**: Copy template and modify
2. **Test locally**: Run migration on development database
3. **Add to version control**: Commit the migration file
4. **Deploy**: Run migrations on staging/production
5. **Verify**: Check database schema and data integrity
## Troubleshooting
**Migration fails:**
```bash
# Check database connection
ls -la pet_pictures.db
# Check migration table
sqlite3 pet_pictures.db "SELECT * FROM migrations;"
# Run specific migration for debugging
python migrations/001_add_description_column.py
```
**Reset migration tracking:**
```sql
-- Careful! This will re-run all migrations
DELETE FROM migrations WHERE migration_name = '001_add_description_column';
```
## Integration
### Docker
The migration runner is integrated with Docker:
```bash
# Run migrations in Docker
docker compose --profile migrate up migrate
```
### CI/CD
Add to deployment scripts:
```bash
# Run migrations before starting app
python migrations/migrate.py && python main.py
```

1
migrations/__init__.py Normal file
View File

@@ -0,0 +1 @@
# Migrations package

174
migrations/migrate.py Executable file
View File

@@ -0,0 +1,174 @@
#!/usr/bin/env python3
"""
Migration runner for Pet Picture Queue
Handles database schema migrations in order
"""
import os
import sys
import importlib.util
import sqlite3
from datetime import datetime
def get_database_path():
"""Get the correct database path"""
if os.path.exists("pet_pictures.db"):
return "pet_pictures.db"
elif os.path.exists("app/pet_pictures.db"):
return "app/pet_pictures.db"
else:
return "pet_pictures.db"
def create_migration_table(cursor):
"""Create migrations tracking table"""
cursor.execute("""
CREATE TABLE IF NOT EXISTS migrations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
migration_name TEXT NOT NULL UNIQUE,
applied_at TIMESTAMP NOT NULL
)
""")
def get_applied_migrations(cursor):
"""Get list of applied migrations"""
try:
cursor.execute("SELECT migration_name FROM migrations ORDER BY id")
return [row[0] for row in cursor.fetchall()]
except sqlite3.OperationalError:
# Migrations table doesn't exist yet
return []
def get_migration_files():
"""Get list of migration files in order"""
migrations_dir = os.path.dirname(__file__)
migration_files = []
for file in os.listdir(migrations_dir):
if file.endswith('.py') and file != 'migrate.py' and file != '__init__.py':
migration_files.append(file)
# Sort by filename (assumes numbered naming like 001_xxx.py)
migration_files.sort()
return migration_files
def load_migration_module(migration_file):
"""Load a migration module dynamically"""
migrations_dir = os.path.dirname(__file__)
file_path = os.path.join(migrations_dir, migration_file)
spec = importlib.util.spec_from_file_location("migration", file_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def run_migrations():
"""Run pending migrations"""
db_path = get_database_path()
print(f"Using database: {db_path}")
try:
db = sqlite3.connect(db_path)
cursor = db.cursor()
# Create migrations table
create_migration_table(cursor)
db.commit()
# Get current state
applied_migrations = get_applied_migrations(cursor)
available_migrations = get_migration_files()
print(f"Applied migrations: {len(applied_migrations)}")
print(f"Available migrations: {len(available_migrations)}")
# Find pending migrations
pending_migrations = []
for migration_file in available_migrations:
migration_name = migration_file.replace('.py', '')
if migration_name not in applied_migrations:
pending_migrations.append(migration_file)
if not pending_migrations:
print("✓ No pending migrations")
return 0
print(f"\nRunning {len(pending_migrations)} pending migrations:")
# Run each pending migration
for migration_file in pending_migrations:
migration_name = migration_file.replace('.py', '')
print(f"\n📦 Running migration: {migration_name}")
try:
# Load and run migration
module = load_migration_module(migration_file)
if hasattr(module, 'migrate_up'):
success = module.migrate_up(cursor)
if success is not False: # Allow None or True
# Mark as applied
cursor.execute(
"INSERT INTO migrations (migration_name, applied_at) VALUES (?, ?)",
(migration_name, datetime.now())
)
db.commit()
print(f"✓ Migration {migration_name} applied successfully")
else:
print(f"• Migration {migration_name} skipped (already applied)")
else:
print(f"❌ Migration {migration_name} has no migrate_up function")
except Exception as e:
print(f"❌ Migration {migration_name} failed: {e}")
db.rollback()
return 1
print(f"\n🎉 All migrations completed successfully!")
# Show final schema
cursor.execute("PRAGMA table_info(pet_pictures)")
columns = cursor.fetchall()
print(f"\nFinal pet_pictures schema:")
for col in columns:
nullable = "NOT NULL" if col[3] else "NULL"
default = f"DEFAULT {col[4]}" if col[4] else ""
print(f" {col[1]} {col[2]} {nullable} {default}")
return 0
except Exception as e:
print(f"❌ Migration runner failed: {e}")
return 1
finally:
if 'db' in locals():
db.close()
def main():
"""Main entry point"""
print("=" * 60)
print("Pet Picture Queue - Database Migration Runner")
print(f"Started at: {datetime.now().isoformat()}")
print("=" * 60)
result = run_migrations()
print("=" * 60)
if result == 0:
print("✅ Migration process completed successfully!")
else:
print("❌ Migration process failed!")
print("=" * 60)
return result
if __name__ == "__main__":
sys.exit(main())