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