Prod Deployment Preparation

This commit is contained in:
Koncept Kit
2026-01-04 19:08:54 +07:00
parent 85199958bc
commit 6ec0745966
16 changed files with 2188 additions and 5 deletions

1
alembic/README Normal file
View File

@@ -0,0 +1 @@
Generic single-database configuration.

259
alembic/README.md Normal file
View File

@@ -0,0 +1,259 @@
# Alembic Database Migrations
This directory contains **Alembic** database migrations for the LOAF membership platform.
## What is Alembic?
Alembic is a lightweight database migration tool for SQLAlchemy. It allows you to:
- Track database schema changes over time
- Apply migrations incrementally
- Roll back changes if needed
- Auto-generate migration scripts from model changes
## Directory Structure
```
alembic/
├── versions/ # Migration scripts (KEEP IN VERSION CONTROL)
│ └── *.py # Individual migration files
├── env.py # Alembic environment configuration
├── script.py.mako # Template for new migration files
└── README.md # This file
```
## Quick Start
### 1. Create a New Migration
After making changes to `models.py`, generate a migration:
```bash
cd backend
alembic revision --autogenerate -m "add_user_bio_field"
```
This will create a new file in `alembic/versions/` like:
```
3e02c74581c9_add_user_bio_field.py
```
### 2. Review the Generated Migration
**IMPORTANT:** Always review auto-generated migrations before applying them!
```bash
# Open the latest migration file
cat alembic/versions/3e02c74581c9_add_user_bio_field.py
```
Check:
- ✅ The `upgrade()` function contains the correct changes
- ✅ The `downgrade()` function properly reverses those changes
- ✅ No unintended table drops or data loss
### 3. Apply the Migration
```bash
# Apply all pending migrations
alembic upgrade head
# Or apply migrations one at a time
alembic upgrade +1
```
### 4. Rollback a Migration
```bash
# Rollback the last migration
alembic downgrade -1
# Rollback to a specific revision
alembic downgrade 3e02c74581c9
```
## Common Commands
| Command | Description |
|---------|-------------|
| `alembic current` | Show current migration revision |
| `alembic history` | Show migration history |
| `alembic heads` | Show head revisions |
| `alembic upgrade head` | Apply all pending migrations |
| `alembic downgrade -1` | Rollback last migration |
| `alembic revision --autogenerate -m "message"` | Create new migration |
| `alembic stamp head` | Mark database as up-to-date without running migrations |
## Migration Workflow
### For Development
1. **Make changes to `models.py`**
```python
# In models.py
class User(Base):
# ...existing fields...
bio = Column(Text, nullable=True) # New field
```
2. **Generate migration**
```bash
alembic revision --autogenerate -m "add_user_bio_field"
```
3. **Review the generated file**
```python
# In alembic/versions/xxxxx_add_user_bio_field.py
def upgrade():
op.add_column('users', sa.Column('bio', sa.Text(), nullable=True))
def downgrade():
op.drop_column('users', 'bio')
```
4. **Apply migration**
```bash
alembic upgrade head
```
5. **Commit migration file to Git**
```bash
git add alembic/versions/xxxxx_add_user_bio_field.py
git commit -m "Add user bio field"
```
### For Production Deployment
**Fresh Database (New Installation):**
```bash
# 1. Create database
createdb membership_db
# 2. Run initial schema SQL (creates all 17 tables)
psql -U username -d membership_db -f ../migrations/000_initial_schema.sql
# 3. Mark database as up-to-date with Alembic
alembic stamp head
# 4. Verify
alembic current # Should show: 001_initial_baseline (head)
```
**Existing Database (Apply New Migrations):**
```bash
# 1. Pull latest code
git pull origin main
# 2. Apply migrations
alembic upgrade head
# 3. Verify
alembic current
# 4. Restart application
systemctl restart membership-backend
```
## Configuration
### Database Connection
Alembic reads the `DATABASE_URL` from your `.env` file:
```env
DATABASE_URL=postgresql://user:password@localhost:5432/membership_db
```
The connection is configured in `alembic/env.py` (lines 29-36).
### Target Metadata
Alembic uses `Base.metadata` from `models.py` to detect changes:
```python
# In alembic/env.py
from models import Base
target_metadata = Base.metadata
```
## Important Notes
### ✅ DO:
- Always review auto-generated migrations before applying
- Test migrations in development before production
- Commit migration files to version control
- Write descriptive migration messages
- Include both `upgrade()` and `downgrade()` functions
### ❌ DON'T:
- Don't edit migration files after they've been applied in production
- Don't delete migration files from `alembic/versions/`
- Don't modify the `revision` or `down_revision` values
- Don't commit `.pyc` files (already in .gitignore)
## Migration History
| Revision | Description | Date | Type |
|----------|-------------|------|------|
| `001_initial_baseline` | Baseline marker (empty migration) | 2026-01-02 | Baseline |
**Note:** The actual initial schema is created by running `backend/migrations/000_initial_schema.sql`. The baseline migration is an empty marker that indicates the starting point for Alembic tracking.
## Troubleshooting
### "Target database is not up to date"
```bash
# Check current revision
alembic current
# Check pending migrations
alembic history
# Apply missing migrations
alembic upgrade head
```
### "FAILED: Can't locate revision identified by 'xxxxx'"
The database thinks it's at a revision that doesn't exist in your `alembic/versions/`.
**Solution:**
```bash
# Mark database at a known good revision
alembic stamp head
```
### Migration conflicts
If you get merge conflicts in migration files:
1. Resolve conflicts in the migration file
2. Ensure `revision` and `down_revision` chain is correct
3. Test the migration locally
### Fresh database setup
For a completely new database:
```bash
# Step 1: Run initial schema SQL
psql -U username -d membership_db -f ../migrations/000_initial_schema.sql
# Step 2: Mark as up-to-date
alembic stamp head
# Step 3: Verify
alembic current # Should show: 001_initial_baseline (head)
```
## Legacy Migrations
Old numbered SQL migrations (`000_initial_schema.sql` through `011_wordpress_import_enhancements.sql`) are preserved in `backend/migrations/` for reference. These have been consolidated into the initial Alembic migration.
**Going forward, all new migrations must use Alembic.**
## Additional Resources
- [Alembic Documentation](https://alembic.sqlalchemy.org/)
- [SQLAlchemy Documentation](https://docs.sqlalchemy.org/)
- [PostgreSQL Documentation](https://www.postgresql.org/docs/)

96
alembic/env.py Normal file
View File

@@ -0,0 +1,96 @@
import os
import sys
from logging.config import fileConfig
from sqlalchemy import engine_from_config, pool
from alembic import context
from dotenv import load_dotenv
# Add the parent directory to the path so we can import our models
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
# Load environment variables from .env file
load_dotenv()
# Import all models so Alembic can detect them
from models import Base
import models # This ensures all models are imported
# 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.
if config.config_file_name is not None:
fileConfig(config.config_file_name)
# Set the SQLAlchemy URL from environment variable
database_url = os.getenv("DATABASE_URL")
if database_url:
config.set_main_option("sqlalchemy.url", database_url)
else:
raise ValueError(
"DATABASE_URL environment variable not set. "
"Please create a .env file with DATABASE_URL=postgresql://user:password@host:port/dbname"
)
# Add your model's MetaData object here for 'autogenerate' support
target_metadata = Base.metadata
def run_migrations_offline() -> None:
"""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,
dialect_opts={"paramstyle": "named"},
compare_type=True, # Detect type changes
compare_server_default=True, # Detect default value changes
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
compare_type=True, # Detect type changes
compare_server_default=True, # Detect default value changes
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

26
alembic/script.py.mako Normal file
View File

@@ -0,0 +1,26 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision: str = ${repr(up_revision)}
down_revision: Union[str, None] = ${repr(down_revision)}
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
def upgrade() -> None:
${upgrades if upgrades else "pass"}
def downgrade() -> None:
${downgrades if downgrades else "pass"}

View File

@@ -0,0 +1,59 @@
"""initial_baseline - Use 000_initial_schema.sql for fresh deployments
Revision ID: 001_initial_baseline
Revises:
Create Date: 2026-01-02 16:45:00.000000
IMPORTANT: This is a baseline migration for existing databases.
For FRESH deployments:
1. Run: psql -U user -d dbname -f backend/migrations/000_initial_schema.sql
2. Run: alembic stamp head
For EXISTING deployments (already have database):
1. Run: alembic stamp head (marks database as up-to-date)
This migration intentionally does NOTHING because:
- Fresh deployments use 000_initial_schema.sql to create all tables
- Existing deployments already have all tables from 000_initial_schema.sql
- Future migrations will be incremental changes from this baseline
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '001_initial_baseline'
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""
This migration does nothing.
It serves as a baseline marker that indicates:
- All 17 tables exist (users, events, subscriptions, etc.)
- All 8 enums are defined (UserStatus, UserRole, etc.)
- All indexes and constraints are in place
The actual schema is created by running:
backend/migrations/000_initial_schema.sql
"""
pass
def downgrade() -> None:
"""
Cannot downgrade below baseline.
If you need to completely reset the database:
1. dropdb dbname
2. createdb dbname
3. psql -U user -d dbname -f backend/migrations/000_initial_schema.sql
4. alembic stamp head
"""
pass