Fixes critical issue where migration 002 indexes already existed in SCHEMA_SQL, causing 'index already exists' errors on databases created before v1.0.0-rc.1. Changes: - Removed duplicate index definitions from SCHEMA_SQL (database.py) - Enhanced migration system to detect and handle indexes properly - Added comprehensive documentation of the fix Version bumped to 1.0.0-rc.2 with full changelog entry. Refs: docs/reports/2025-11-24-migration-fix-v1.0.0-rc.2.md
11 KiB
Initial Schema SQL Implementation Guide
Overview
This guide provides step-by-step instructions for implementing the INITIAL_SCHEMA_SQL constant and updating the database initialization system as specified in ADR-032.
Priority: CRITICAL for v1.1.0 Estimated Time: 4-6 hours Risk Level: Low (backward compatible)
Pre-Implementation Checklist
- Read ADR-031 (Database Migration System Redesign)
- Read ADR-032 (Initial Schema SQL Implementation)
- Review current migrations in
/migrations/directory - Backup any test databases
- Ensure test environment is ready
Implementation Steps
Step 1: Add INITIAL_SCHEMA_SQL Constant
File: /home/phil/Projects/starpunk/starpunk/database.py
Action: Add the following constant ABOVE the current SCHEMA_SQL:
# Database schema - V0.1.0 baseline (see ADR-032)
# This represents the initial database structure from commit a68fd57
# All schema evolution happens through migrations from this baseline
INITIAL_SCHEMA_SQL = """
-- Notes metadata (content is in files)
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
slug TEXT UNIQUE NOT NULL,
file_path TEXT UNIQUE NOT NULL,
published BOOLEAN DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP,
content_hash TEXT
);
CREATE INDEX IF NOT EXISTS idx_notes_created_at ON notes(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_notes_published ON notes(published);
CREATE INDEX IF NOT EXISTS idx_notes_slug ON notes(slug);
CREATE INDEX IF NOT EXISTS idx_notes_deleted_at ON notes(deleted_at);
-- Authentication sessions (IndieLogin)
CREATE TABLE IF NOT EXISTS sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_token TEXT UNIQUE NOT NULL,
me TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP NOT NULL,
last_used_at TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_sessions_token ON sessions(session_token);
CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at);
-- Micropub access tokens (original insecure version)
CREATE TABLE IF NOT EXISTS tokens (
token TEXT PRIMARY KEY,
me TEXT NOT NULL,
client_id TEXT,
scope TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_tokens_me ON tokens(me);
-- CSRF state tokens (for IndieAuth flow)
CREATE TABLE IF NOT EXISTS auth_state (
state TEXT PRIMARY KEY,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_auth_state_expires ON auth_state(expires_at);
"""
Step 2: Rename Current SCHEMA_SQL
File: /home/phil/Projects/starpunk/starpunk/database.py
Action: Rename the existing SCHEMA_SQL constant and add documentation:
# Current database schema - FOR DOCUMENTATION ONLY
# This shows the current complete schema after all migrations
# NOT used for database initialization - see INITIAL_SCHEMA_SQL
# Updated by migrations 001 and 002
CURRENT_SCHEMA_SQL = """
[existing SCHEMA_SQL content]
"""
Step 3: Add Helper Function
File: /home/phil/Projects/starpunk/starpunk/database.py
Action: Add this function before init_db():
def database_exists_with_tables(db_path):
"""
Check if database exists and has tables
Args:
db_path: Path to SQLite database file
Returns:
bool: True if database exists with at least one table
"""
import os
# Check if file exists
if not os.path.exists(db_path):
return False
# Check if it has tables
try:
conn = sqlite3.connect(db_path)
cursor = conn.execute(
"SELECT COUNT(*) FROM sqlite_master WHERE type='table'"
)
table_count = cursor.fetchone()[0]
conn.close()
return table_count > 0
except Exception:
return False
Step 4: Update init_db() Function
File: /home/phil/Projects/starpunk/starpunk/database.py
Action: Replace the init_db() function with:
def init_db(app=None):
"""
Initialize database schema and run migrations
For fresh databases:
1. Creates v0.1.0 baseline schema (INITIAL_SCHEMA_SQL)
2. Runs all migrations to bring to current version
For existing databases:
1. Skips schema creation (tables already exist)
2. Runs only pending migrations
Args:
app: Flask application instance (optional, for config access)
"""
if app:
db_path = app.config["DATABASE_PATH"]
logger = app.logger
else:
# Fallback to default path
db_path = Path("./data/starpunk.db")
logger = logging.getLogger(__name__)
# Ensure parent directory exists
db_path.parent.mkdir(parents=True, exist_ok=True)
# Check if this is an existing database
if database_exists_with_tables(db_path):
# Existing database - skip schema creation, only run migrations
logger.info(f"Existing database found: {db_path}")
logger.info("Running pending migrations...")
else:
# Fresh database - create initial v0.1.0 schema
logger.info(f"Creating new database: {db_path}")
conn = sqlite3.connect(db_path)
try:
# Create v0.1.0 baseline schema
conn.executescript(INITIAL_SCHEMA_SQL)
conn.commit()
logger.info("Created initial v0.1.0 database schema")
except Exception as e:
logger.error(f"Failed to create initial schema: {e}")
raise
finally:
conn.close()
# Run migrations (for both fresh and existing databases)
# This will apply ALL migrations for fresh databases,
# or only pending migrations for existing databases
from starpunk.migrations import run_migrations
try:
run_migrations(db_path, logger)
except Exception as e:
logger.error(f"Migration failed: {e}")
raise
Step 5: Update Tests
File: /home/phil/Projects/starpunk/tests/test_migrations.py
Add these test cases:
def test_fresh_database_initialization(tmp_path):
"""Test that fresh database gets initial schema then migrations"""
db_path = tmp_path / "test.db"
# Initialize fresh database
init_db_with_path(db_path)
# Verify initial tables exist
conn = sqlite3.connect(db_path)
cursor = conn.execute(
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
)
tables = [row[0] for row in cursor.fetchall()]
# Should have all tables including migration tracking
assert "notes" in tables
assert "sessions" in tables
assert "tokens" in tables
assert "auth_state" in tables
assert "schema_migrations" in tables
assert "authorization_codes" in tables # Added by migration 002
# Verify migrations were applied
cursor = conn.execute("SELECT COUNT(*) FROM schema_migrations")
migration_count = cursor.fetchone()[0]
assert migration_count >= 2 # At least migrations 001 and 002
conn.close()
def test_existing_database_upgrade(tmp_path):
"""Test that existing database only runs pending migrations"""
db_path = tmp_path / "test.db"
# Create a database with v0.1.0 schema manually
conn = sqlite3.connect(db_path)
conn.executescript(INITIAL_SCHEMA_SQL)
conn.commit()
conn.close()
# Run init_db on existing database
init_db_with_path(db_path)
# Verify migrations were applied
conn = sqlite3.connect(db_path)
# Check that migration 001 was applied (code_verifier column)
cursor = conn.execute("PRAGMA table_info(auth_state)")
columns = [row[1] for row in cursor.fetchall()]
assert "code_verifier" in columns
# Check that migration 002 was applied (authorization_codes table)
cursor = conn.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name='authorization_codes'"
)
assert cursor.fetchone() is not None
conn.close()
Step 6: Manual Testing Procedure
-
Test Fresh Database:
# Backup existing database mv data/starpunk.db data/starpunk.db.backup # Start application (will create fresh database) uv run python app.py # Verify application starts without errors # Check logs for "Created initial v0.1.0 database schema" # Check logs for "Applied migration: 001_add_code_verifier_to_auth_state.sql" # Check logs for "Applied migration: 002_secure_tokens_and_authorization_codes.sql" -
Test Existing Database:
# Restore backup cp data/starpunk.db.backup data/starpunk.db # Start application uv run python app.py # Verify application starts without errors # Check logs for "Existing database found" # Check logs for migration status -
Test Database Queries:
sqlite3 data/starpunk.db # Check tables .tables # Check schema_migrations SELECT * FROM schema_migrations; # Verify authorization_codes table exists .schema authorization_codes # Verify tokens table has token_hash column .schema tokens
Step 7: Update Documentation
File: /home/phil/Projects/starpunk/docs/architecture/database.md
Add section:
## Schema Evolution Strategy
StarPunk uses a baseline + migrations approach for schema management:
1. **INITIAL_SCHEMA_SQL**: Represents the v0.1.0 baseline schema
2. **Migrations**: All schema changes applied sequentially
3. **CURRENT_SCHEMA_SQL**: Documentation of current complete schema
This ensures:
- Predictable upgrade paths from any version
- Clear schema history through migrations
- Testable database evolution
Validation Checklist
After implementation, verify:
- Fresh database initialization works
- Existing database upgrade works
- No duplicate index/table errors
- All tests pass
- Application starts normally
- Can create/read/update notes
- Authentication still works
- Micropub endpoint functional
Troubleshooting
Issue: "table already exists" error
Solution: Check that database_exists_with_tables() is working correctly
Issue: "no such column" error
Solution: Verify INITIAL_SCHEMA_SQL matches v0.1.0 exactly
Issue: Migrations not running
Solution: Check migrations/ directory path and file permissions
Issue: Tests failing
Solution: Ensure test database is properly isolated from production
Rollback Procedure
If issues occur:
- Restore database backup
- Revert code changes
- Document issue in ADR-032
- Re-plan implementation
Post-Implementation
- Update CHANGELOG.md
- Update version number to 1.1.0-rc.1
- Create release notes
- Test Docker container with new schema
- Document any discovered edge cases
Contact for Questions
If you encounter issues not covered in this guide:
- Review ADR-031 and ADR-032
- Check existing migration test cases
- Review git history for database.py evolution
- Document any new findings in /docs/reports/
Created: 2025-11-24 For: StarPunk v1.1.0 Priority: CRITICAL