# Migration System Implementation Guidance **Date**: 2025-11-19 **Architect**: StarPunk Architect **Developer**: StarPunk Developer **Status**: Ready for Implementation ## Executive Summary All 7 critical questions have been answered with decisive architectural decisions. The implementation is straightforward and production-ready. ## Critical Decisions Summary | # | Question | Decision | Action Required | |---|----------|----------|-----------------| | **1** | Chicken-and-egg problem | Fresh database detection | Add `is_schema_current()` to migrations.py | | **2** | schema_migrations location | Only in migrations.py | No changes needed (already correct) | | **3** | ALTER TABLE idempotency | Accept non-idempotency | No changes needed (tracking handles it) | | **4** | Filename validation | Flexible glob + sort | No changes needed (already implemented) | | **5** | Existing database path | Automatic via heuristic | Handled by Q1 solution | | **6** | Column helpers | Provide as advanced utils | Add 3 helper functions to migrations.py | | **7** | SCHEMA_SQL purpose | Complete target state | No changes needed (already correct) | ## Implementation Checklist ### Step 1: Add Helper Functions to `starpunk/migrations.py` Add these three utility functions (for advanced usage, not required for migration 001): ```python def table_exists(conn, table_name): """Check if table exists in database""" cursor = conn.execute( "SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table_name,) ) return cursor.fetchone() is not None def column_exists(conn, table_name, column_name): """Check if column exists in table""" try: cursor = conn.execute(f"PRAGMA table_info({table_name})") columns = [row[1] for row in cursor.fetchall()] return column_name in columns except sqlite3.OperationalError: return False def index_exists(conn, index_name): """Check if index exists in database""" cursor = conn.execute( "SELECT name FROM sqlite_master WHERE type='index' AND name=?", (index_name,) ) return cursor.fetchone() is not None ``` ### Step 2: Add Fresh Database Detection Add this function before `run_migrations()`: ```python def is_schema_current(conn): """ Check if database schema is current (matches SCHEMA_SQL) Uses heuristic: Check for presence of latest schema features Currently checks for code_verifier column in auth_state table Args: conn: SQLite connection Returns: bool: True if schema appears current, False if legacy """ try: cursor = conn.execute("PRAGMA table_info(auth_state)") columns = [row[1] for row in cursor.fetchall()] return 'code_verifier' in columns except sqlite3.OperationalError: # Table doesn't exist - definitely not current return False ``` **Important**: This heuristic checks for `code_verifier` column. When you add future migrations, update this function to check for the latest schema feature. ### Step 3: Modify `run_migrations()` Function Replace the migration application logic with fresh database detection: **Find this section** (after `create_migrations_table(conn)`): ```python # Get already-applied migrations applied = get_applied_migrations(conn) # Discover migration files migration_files = discover_migration_files(migrations_dir) if not migration_files: logger.info("No migration files found") return # Apply pending migrations pending_count = 0 for migration_name, migration_path in migration_files: if migration_name not in applied: apply_migration(conn, migration_name, migration_path, logger) pending_count += 1 ``` **Replace with**: ```python # Check if this is a fresh database with current schema cursor = conn.execute("SELECT COUNT(*) FROM schema_migrations") migration_count = cursor.fetchone()[0] # Discover migration files migration_files = discover_migration_files(migrations_dir) if not migration_files: logger.info("No migration files found") return # Fresh database detection if migration_count == 0: if is_schema_current(conn): # Schema is current - mark all migrations as applied for migration_name, _ in migration_files: conn.execute( "INSERT INTO schema_migrations (migration_name) VALUES (?)", (migration_name,) ) conn.commit() logger.info( f"Fresh database detected: marked {len(migration_files)} " f"migrations as applied (schema already current)" ) return else: logger.info("Legacy database detected: applying all migrations") # Get already-applied migrations applied = get_applied_migrations(conn) # Apply pending migrations pending_count = 0 for migration_name, migration_path in migration_files: if migration_name not in applied: apply_migration(conn, migration_name, migration_path, logger) pending_count += 1 ``` ## Files That Need Changes 1. **`/home/phil/Projects/starpunk/starpunk/migrations.py`** - Add `is_schema_current()` function - Add `table_exists()` helper - Add `column_exists()` helper - Add `index_exists()` helper - Modify `run_migrations()` to include fresh database detection 2. **No other files need changes** - `SCHEMA_SQL` is correct (includes code_verifier) - Migration 001 is correct (adds code_verifier) - `database.py` is correct (calls run_migrations) ## Test Scenarios After implementation, verify these scenarios: ### Test 1: Fresh Database (New Install) ```bash rm data/starpunk.db uv run flask --app app.py run ``` **Expected Log Output**: ``` [INFO] Database initialized: data/starpunk.db [INFO] Fresh database detected: marked 1 migrations as applied (schema already current) ``` **Verify**: ```bash sqlite3 data/starpunk.db "SELECT * FROM schema_migrations;" # Should show: 1|001_add_code_verifier_to_auth_state.sql| sqlite3 data/starpunk.db "PRAGMA table_info(auth_state);" # Should include code_verifier column ``` ### Test 2: Legacy Database (Before PKCE Feature) ```bash # Create old database without code_verifier sqlite3 data/starpunk.db " CREATE TABLE auth_state ( state TEXT PRIMARY KEY, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, expires_at TIMESTAMP NOT NULL, redirect_uri TEXT ); " uv run flask --app app.py run ``` **Expected Log Output**: ``` [INFO] Database initialized: data/starpunk.db [INFO] Legacy database detected: applying all migrations [INFO] Applied migration: 001_add_code_verifier_to_auth_state.sql [INFO] Migrations complete: 1 applied, 1 total ``` **Verify**: ```bash sqlite3 data/starpunk.db "PRAGMA table_info(auth_state);" # Should now include code_verifier column ``` ### Test 3: Current Database (Already Has code_verifier, No Migration Tracking) ```bash # Simulate database created after PKCE but before migrations rm data/starpunk.db # Run once to create current schema uv run flask --app app.py run # Delete migration tracking to simulate upgrade scenario sqlite3 data/starpunk.db "DROP TABLE schema_migrations;" # Now run again (simulates upgrade) uv run flask --app app.py run ``` **Expected Log Output**: ``` [INFO] Database initialized: data/starpunk.db [INFO] Fresh database detected: marked 1 migrations as applied (schema already current) ``` **Verify**: Migration 001 should NOT execute (would fail on duplicate column). ### Test 4: Up-to-Date Database ```bash # Database already migrated uv run flask --app app.py run ``` **Expected Log Output**: ``` [INFO] Database initialized: data/starpunk.db [INFO] All migrations up to date (1 total) ``` ## Edge Cases Handled 1. **Fresh install**: SCHEMA_SQL creates complete schema, migrations marked as applied, never executed ✓ 2. **Upgrade from pre-PKCE**: Migration 001 executes, adds code_verifier ✓ 3. **Upgrade from post-PKCE, pre-migrations**: Fresh DB detection marks migrations as applied ✓ 4. **Re-running on current database**: Idempotent, no changes ✓ 5. **Migration already applied**: Skipped via tracking table ✓ ## Future Migration Pattern When adding future schema changes: 1. **Update SCHEMA_SQL** in `database.py` with new tables/columns 2. **Create migration file** `002_description.sql` with same SQL 3. **Update `is_schema_current()`** to check for new feature (latest heuristic) 4. **Test with all 4 scenarios above** Example for adding tags feature: **`database.py` SCHEMA_SQL**: ```python # Add at end of SCHEMA_SQL CREATE TABLE IF NOT EXISTS tags ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE NOT NULL ); ``` **`migrations/002_add_tags_table.sql`**: ```sql -- Migration: Add tags table -- Date: 2025-11-20 CREATE TABLE IF NOT EXISTS tags ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE NOT NULL ); ``` **Update `is_schema_current()`**: ```python def is_schema_current(conn): """Check if database schema is current""" try: # Check for latest feature (tags table in this case) return table_exists(conn, 'tags') except sqlite3.OperationalError: return False ``` ## Key Architectural Principles 1. **SCHEMA_SQL is the destination**: It represents complete current state 2. **Migrations are the journey**: They get existing databases to that state 3. **Fresh databases skip the journey**: They're already at the destination 4. **Heuristic detection is sufficient**: Check for latest feature to determine currency 5. **Migration tracking is the safety net**: Prevents re-running migrations 6. **Idempotency is nice-to-have**: Tracking is the primary mechanism ## Common Pitfalls to Avoid 1. **Don't remove from SCHEMA_SQL**: Only add, never remove (even if you "undo" via migration) 2. **Don't create migration without SCHEMA_SQL update**: They must stay in sync 3. **Don't hardcode schema checks**: Use `is_schema_current()` heuristic 4. **Don't forget to update heuristic**: When adding new migrations, update the check 5. **Don't make migrations complex**: Keep them simple, let tracking handle safety ## Questions? All architectural decisions are documented in: - `/home/phil/Projects/starpunk/docs/decisions/ADR-020-automatic-database-migrations.md` See the "Developer Questions & Architectural Responses" section for detailed rationale on all 7 questions. ## Ready to Implement You have: - Clear implementation steps - Complete code examples - Test scenarios - Edge case handling - Future migration pattern Proceed with implementation. The architecture is solid and production-ready. --- **Architect Sign-Off**: Ready for implementation **Next Step**: Developer implements modifications to `migrations.py`