Skip to content

Commit 8da02c3

Browse files
committed
chore: api alembic
1 parent a27eeda commit 8da02c3

File tree

2 files changed

+128
-0
lines changed

2 files changed

+128
-0
lines changed
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
from __future__ import with_statement
2+
import os
3+
from logging.config import fileConfig
4+
5+
from sqlalchemy import engine_from_config
6+
from sqlalchemy import pool
7+
8+
from alembic import context
9+
10+
# this is the Alembic Config object, which provides
11+
# access to the values within the .ini file in use.
12+
config = context.config
13+
14+
# Interpret the config file for Python logging. Some environments (CI or
15+
# trimmed alembic.ini) may not include all logger sections; guard against
16+
# that to avoid stopping migrations with a KeyError.
17+
if config.config_file_name is not None:
18+
try:
19+
fileConfig(config.config_file_name)
20+
except Exception:
21+
# Fall back to a minimal logging configuration if the ini is missing
22+
# expected logger sections (e.g. 'logger_sqlalchemy'). This makes
23+
# migrations resilient when run in different environments.
24+
import logging
25+
26+
logging.basicConfig(level=logging.INFO)
27+
28+
target_metadata = None
29+
30+
# Use DATABASE_URL env if provided
31+
db_url = os.getenv("DATABASE_URL") or config.get_main_option("sqlalchemy.url")
32+
if db_url:
33+
# Only set the option if we have a valid string value. Avoid setting None
34+
# which causes ConfigParser type errors (option values must be strings).
35+
config.set_main_option("sqlalchemy.url", str(db_url))
36+
37+
38+
def run_migrations_offline():
39+
url = config.get_main_option("sqlalchemy.url")
40+
context.configure(url=url, target_metadata=target_metadata, literal_binds=True)
41+
42+
with context.begin_transaction():
43+
context.run_migrations()
44+
45+
46+
def run_migrations_online():
47+
# Determine whether the configured URL uses an async driver. If so,
48+
# create an AsyncEngine and run the migrations inside an async context
49+
# while delegating the actual migration steps to a sync callable via
50+
# `connection.run_sync`. Otherwise, fall back to the classic sync path.
51+
url = config.get_main_option("sqlalchemy.url")
52+
53+
def _do_run_migrations(connection):
54+
context.configure(connection=connection, target_metadata=target_metadata)
55+
with context.begin_transaction():
56+
context.run_migrations()
57+
58+
if url and url.startswith("postgresql+asyncpg"):
59+
# Async migration path
60+
from sqlalchemy.ext.asyncio import create_async_engine
61+
import asyncio
62+
63+
async_engine = create_async_engine(url, future=True)
64+
65+
async def run():
66+
async with async_engine.connect() as connection:
67+
await connection.run_sync(_do_run_migrations)
68+
69+
asyncio.run(run())
70+
else:
71+
# Sync migration path (classic)
72+
connectable = engine_from_config(
73+
config.get_section(config.config_ini_section),
74+
prefix="sqlalchemy.",
75+
poolclass=pool.NullPool,
76+
)
77+
78+
with connectable.connect() as connection:
79+
_do_run_migrations(connection)
80+
81+
82+
if context.is_offline_mode():
83+
run_migrations_offline()
84+
else:
85+
run_migrations_online()
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"""initial create users and backtest_jobs tables
2+
3+
Revision ID: 0001_initial_create_users_and_jobs
4+
Revises:
5+
Create Date: 2025-11-17
6+
"""
7+
from alembic import op
8+
import sqlalchemy as sa
9+
10+
11+
# revision identifiers, used by Alembic.
12+
revision = '0001_initial_create_users_and_jobs'
13+
down_revision = None
14+
branch_labels = None
15+
depends_on = None
16+
17+
18+
def upgrade():
19+
op.create_table(
20+
'users',
21+
sa.Column('id', sa.Integer(), primary_key=True),
22+
sa.Column('username', sa.String(length=128), nullable=False, unique=True, index=True),
23+
sa.Column('hashed_password', sa.String(length=256), nullable=False),
24+
sa.Column('is_active', sa.Boolean(), nullable=False, server_default=sa.text('true')),
25+
sa.Column('role', sa.String(length=32), nullable=False, server_default='user'),
26+
sa.Column('created_at', sa.DateTime(), server_default=sa.func.now()),
27+
)
28+
29+
op.create_table(
30+
'backtest_jobs',
31+
sa.Column('id', sa.String(length=64), primary_key=True),
32+
sa.Column('user_id', sa.Integer(), sa.ForeignKey('users.id'), nullable=True),
33+
sa.Column('status', sa.String(length=32), nullable=False, server_default='queued'),
34+
sa.Column('params', sa.JSON(), nullable=True),
35+
sa.Column('result_path', sa.String(length=1024), nullable=True),
36+
sa.Column('created_at', sa.DateTime(), server_default=sa.func.now()),
37+
sa.Column('updated_at', sa.DateTime(), server_default=sa.func.now(), onupdate=sa.func.now()),
38+
)
39+
40+
41+
def downgrade():
42+
op.drop_table('backtest_jobs')
43+
op.drop_table('users')

0 commit comments

Comments
 (0)