feat(db): fix Alembic migrations and add documentation (#7) - SQLite batch mode for constraints, migrations README

This commit is contained in:
Andrew Kaszubski 2025-12-26 14:50:09 +11:00
parent 1ea006e41f
commit 68be12c451
2 changed files with 182 additions and 58 deletions

123
migrations/README.md Normal file
View File

@ -0,0 +1,123 @@
# Database Migrations
This directory contains Alembic database migrations for TradingAgents.
## Quick Start
```bash
# Apply all migrations
alembic upgrade head
# Check current version
alembic current
# View migration history
alembic history
```
## Migration Commands
### Applying Migrations
```bash
# Apply all pending migrations
alembic upgrade head
# Apply to a specific revision
alembic upgrade 003
# Apply next migration only
alembic upgrade +1
```
### Rolling Back Migrations
```bash
# Roll back one migration
alembic downgrade -1
# Roll back to specific revision
alembic downgrade 002
# Roll back all migrations
alembic downgrade base
```
### Checking Status
```bash
# Show current revision
alembic current
# Show migration history
alembic history
# Show pending migrations
alembic heads
```
## Creating New Migrations
### Auto-generate from Model Changes
After modifying models in `tradingagents/api/models/`:
```bash
# Generate migration from model changes
alembic revision --autogenerate -m "Add new_feature table"
```
### Manual Migration
```bash
# Create empty migration
alembic revision -m "Manual migration description"
```
Then edit the generated file in `migrations/versions/`.
## Migration Files
| Revision | Description |
|----------|-------------|
| 001 | Initial migration - Users and Strategies tables |
| 002 | User profile fields - tax_jurisdiction, timezone, api_key_hash |
| 003 | Portfolio model - live, paper, backtest types |
| 004 | Settings model - risk profiles, alert preferences |
| 005 | Trade model - execution history with CGT tracking |
## SQLite Compatibility
This project uses SQLite by default. For operations that SQLite doesn't support
natively (like ALTER CONSTRAINT), use batch mode:
```python
with op.batch_alter_table('table_name') as batch_op:
batch_op.add_column(sa.Column('new_col', sa.String(50)))
batch_op.create_unique_constraint('uq_name', ['column'])
```
## Best Practices
1. **Always test migrations locally** before committing
2. **Include both upgrade() and downgrade()** functions
3. **Use meaningful revision messages**
4. **Add docstrings** explaining what the migration does
5. **Use batch mode** for SQLite compatibility when needed
6. **Create indexes** for foreign keys and frequently queried columns
## Troubleshooting
### "Target database is not up to date"
```bash
alembic stamp head # Mark current DB as up-to-date
```
### "Can't locate revision"
Check that all migrations have correct `down_revision` values forming a chain.
### SQLite Constraint Error
Use `batch_alter_table` for constraint operations on SQLite.

View File

@ -11,6 +11,8 @@ This migration adds four new fields to the users table:
- is_verified: Email verification status (default: False)
All existing users will get default values for the new required fields.
Uses batch mode for SQLite compatibility.
"""
from typing import Sequence, Union
@ -33,48 +35,57 @@ def upgrade() -> None:
- timezone defaults to "Australia/Sydney"
- api_key_hash is NULL
- is_verified is False
Uses batch mode for SQLite compatibility with constraints.
"""
# Add tax_jurisdiction column
op.add_column(
'users',
sa.Column(
'tax_jurisdiction',
sa.String(length=10),
nullable=False,
server_default='AU',
comment='Tax jurisdiction code (e.g., US, US-CA, AU-NSW)'
# Use batch_alter_table for SQLite compatibility
with op.batch_alter_table('users', schema=None) as batch_op:
# Add tax_jurisdiction column
batch_op.add_column(
sa.Column(
'tax_jurisdiction',
sa.String(length=10),
nullable=False,
server_default='AU',
)
)
)
# Add timezone column
op.add_column(
'users',
sa.Column(
'timezone',
sa.String(length=50),
nullable=False,
server_default='Australia/Sydney',
comment='IANA timezone identifier (e.g., America/New_York, UTC)'
# Add timezone column
batch_op.add_column(
sa.Column(
'timezone',
sa.String(length=50),
nullable=False,
server_default='Australia/Sydney',
)
)
)
# Add api_key_hash column with unique constraint and index
op.add_column(
'users',
sa.Column(
'api_key_hash',
sa.String(length=255),
nullable=True,
comment='Bcrypt hash of API key for programmatic access'
# Add api_key_hash column
batch_op.add_column(
sa.Column(
'api_key_hash',
sa.String(length=255),
nullable=True,
)
)
)
# Create unique constraint for api_key_hash
op.create_unique_constraint(
'uq_users_api_key_hash',
'users',
['api_key_hash']
)
# Create index for fast lookups
# Add is_verified column
batch_op.add_column(
sa.Column(
'is_verified',
sa.Boolean(),
nullable=False,
server_default='0',
)
)
# Create unique constraint for api_key_hash
batch_op.create_unique_constraint(
'uq_users_api_key_hash',
['api_key_hash']
)
# Create index for fast lookups (can be done outside batch)
op.create_index(
'ix_users_api_key_hash',
'users',
@ -82,34 +93,24 @@ def upgrade() -> None:
unique=False
)
# Add is_verified column
op.add_column(
'users',
sa.Column(
'is_verified',
sa.Boolean(),
nullable=False,
server_default='0',
comment='Whether user email has been verified'
)
)
def downgrade() -> None:
"""Remove tax_jurisdiction, timezone, api_key_hash, and is_verified columns from users table.
WARNING: This will permanently delete data in these columns!
Uses batch mode for SQLite compatibility.
"""
# Remove is_verified column
op.drop_column('users', 'is_verified')
# Remove api_key_hash column (drop index and constraint first)
# Drop index first (outside batch)
op.drop_index('ix_users_api_key_hash', 'users')
op.drop_constraint('uq_users_api_key_hash', 'users', type_='unique')
op.drop_column('users', 'api_key_hash')
# Remove timezone column
op.drop_column('users', 'timezone')
# Use batch_alter_table for SQLite compatibility
with op.batch_alter_table('users', schema=None) as batch_op:
# Drop unique constraint
batch_op.drop_constraint('uq_users_api_key_hash', type_='unique')
# Remove tax_jurisdiction column
op.drop_column('users', 'tax_jurisdiction')
# Remove columns
batch_op.drop_column('is_verified')
batch_op.drop_column('api_key_hash')
batch_op.drop_column('timezone')
batch_op.drop_column('tax_jurisdiction')