Skip to content

Commit b000c4d

Browse files
authored
implement database versioning via alembic (#106)
1 parent 0a0f2eb commit b000c4d

9 files changed

Lines changed: 1504 additions & 1122 deletions

File tree

alembic.ini

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# A generic, single database configuration.
2+
3+
[alembic]
4+
# path to migration scripts.
5+
script_location = %(here)s/alembic
6+
7+
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
8+
# Uncomment the line below if you want the files to be prepended with date and time
9+
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
10+
# for all available tokens
11+
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
12+
13+
prepend_sys_path = .
14+
path_separator = os
15+
16+
# the output encoding used when revision files
17+
# are written from script.py.mako
18+
output_encoding = utf-8
19+
20+
[loggers]
21+
keys = root,sqlalchemy,alembic
22+
23+
[handlers]
24+
keys = console
25+
26+
[formatters]
27+
keys = generic
28+
29+
[logger_root]
30+
level = WARNING
31+
handlers = console
32+
qualname =
33+
34+
[logger_sqlalchemy]
35+
level = WARNING
36+
handlers =
37+
qualname = sqlalchemy.engine
38+
39+
[logger_alembic]
40+
level = INFO
41+
handlers =
42+
qualname = alembic
43+
44+
[handler_console]
45+
class = StreamHandler
46+
args = (sys.stderr,)
47+
level = NOTSET
48+
formatter = generic
49+
50+
[formatter_generic]
51+
format = %(levelname)-5.5s [%(name)s] %(message)s
52+
datefmt = %H:%M:%S

alembic/README

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
alembic is used for database migrations (updates)
2+
NOTE: all invocations of `alembic` expect you to run it via `uv run`
3+
4+
### Check current revision
5+
`alembic current`
6+
7+
### List full revision history
8+
`alembic history`
9+
10+
### Upgrade to latest revision
11+
`alembic upgrade head`
12+
13+
### Upgrading to specific revision (can be found in their filename and in the script itself)
14+
`alembic upgrade <revision>`
15+
`alembic upgrade 2d59d4f0c508`
16+
17+
### Downgrade to the first revision
18+
`alembic downgrade base`
19+
20+
### Downgrade to specific revision
21+
`alembic downgrade <revision>`
22+
`alembic downgrade 2d59d4f0c508`
23+
24+
### Generate new revision (make sure you are on the latest revision)
25+
`alembic revision --autogenerate -m "revision message"`

alembic/env.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import asyncio
2+
from logging.config import fileConfig
3+
4+
from sqlalchemy.engine import Connection
5+
6+
from alembic import context
7+
8+
# this is the Alembic Config object, which provides
9+
# access to the values within the .ini file in use.
10+
config = context.config
11+
12+
# Interpret the config file for Python logging.
13+
# This line sets up loggers basically.
14+
if config.config_file_name is not None:
15+
fileConfig(config.config_file_name)
16+
17+
from models import Base
18+
19+
# this is the base we work with
20+
# changs made to it will be compared to the actual database
21+
target_metadata = Base.metadata
22+
23+
from bot import engine
24+
25+
26+
def do_run_migrations(connection: Connection) -> None:
27+
context.configure(connection=connection, target_metadata=target_metadata)
28+
29+
with context.begin_transaction():
30+
context.run_migrations()
31+
32+
33+
async def run_async_migrations() -> None:
34+
global engine
35+
36+
async with engine.connect() as connection:
37+
await connection.run_sync(do_run_migrations)
38+
39+
await engine.dispose()
40+
41+
42+
async def bla() -> None:
43+
async with engine.connect() as connection:
44+
context.configure(connection=connection, target_metadata=target_metadata)
45+
46+
with context.begin_transaction():
47+
context.run_migrations()
48+
49+
50+
def run_migrations_online() -> None:
51+
"""Run migrations in 'online' mode."""
52+
53+
asyncio.run(run_async_migrations())
54+
55+
56+
run_migrations_online()

alembic/script.py.mako

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""${message}
2+
3+
Revision ID: ${up_revision}
4+
Revises: ${down_revision | comma,n}
5+
Create Date: ${create_date}
6+
7+
"""
8+
9+
# disable black for generated scripts
10+
# fmt: off
11+
from typing import Sequence, Union
12+
13+
from alembic import op
14+
import sqlalchemy as sa
15+
${imports if imports else ""}
16+
17+
# revision identifiers, used by Alembic.
18+
revision: str = ${repr(up_revision)}
19+
down_revision: Union[str, None] = ${repr(down_revision)}
20+
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
21+
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
22+
23+
24+
def upgrade() -> None:
25+
"""Upgrade schema."""
26+
${upgrades if upgrades else "pass"}
27+
28+
29+
def downgrade() -> None:
30+
"""Downgrade schema."""
31+
${downgrades if downgrades else "pass"}
32+
33+
34+
# fmt: on
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
"""default model
2+
3+
Revision ID: 4ecfd82a61f0
4+
Revises:
5+
Create Date: 2025-05-29 12:22:28.587473
6+
7+
"""
8+
9+
# disable black for generated scripts
10+
# fmt: off
11+
from typing import Sequence, Union
12+
13+
from alembic import op
14+
import sqlalchemy as sa
15+
16+
17+
# revision identifiers, used by Alembic.
18+
revision: str = '4ecfd82a61f0'
19+
down_revision: Union[str, None] = None
20+
branch_labels: Union[str, Sequence[str], None] = None
21+
depends_on: Union[str, Sequence[str], None] = None
22+
23+
24+
def upgrade() -> None:
25+
"""Upgrade schema."""
26+
# ### commands auto generated by Alembic - please adjust! ###
27+
op.create_table('blacklist',
28+
sa.Column('num', sa.Integer(), nullable=False),
29+
sa.Column('user_id', sa.Integer(), nullable=False),
30+
sa.Column('timestamp', sa.Integer(), nullable=False),
31+
sa.PrimaryKeyConstraint('num')
32+
)
33+
op.create_table('fire',
34+
sa.Column('num', sa.Integer(), autoincrement=True, nullable=False),
35+
sa.Column('reacts', sa.Integer(), nullable=False),
36+
sa.Column('channel_id', sa.Integer(), nullable=False),
37+
sa.Column('message_id', sa.Integer(), nullable=False),
38+
sa.Column('guild_id', sa.Integer(), nullable=False),
39+
sa.Column('user_id', sa.Integer(), nullable=False),
40+
sa.Column('fb_id', sa.Integer(), nullable=False),
41+
sa.Column('message', sa.String(), nullable=True),
42+
sa.Column('attachments', sa.String(), nullable=True),
43+
sa.Column('timestamp', sa.Integer(), nullable=False),
44+
sa.Column('fb_msg_id', sa.Integer(), nullable=True),
45+
sa.Column('emoji', sa.String(), nullable=False),
46+
sa.PrimaryKeyConstraint('num')
47+
)
48+
op.create_table('history',
49+
sa.Column('num', sa.Integer(), autoincrement=True, nullable=False),
50+
sa.Column('user_id', sa.Integer(), nullable=False),
51+
sa.Column('amount', sa.String(), nullable=False),
52+
sa.Column('reason', sa.String(), nullable=False),
53+
sa.Column('time', sa.Integer(), nullable=False),
54+
sa.PrimaryKeyConstraint('num')
55+
)
56+
op.create_index(op.f('ix_history_user_id'), 'history', ['user_id'], unique=False)
57+
op.create_table('ignore',
58+
sa.Column('num', sa.Integer(), nullable=False),
59+
sa.Column('channelID', sa.Integer(), nullable=False),
60+
sa.Column('guildID', sa.Integer(), nullable=False),
61+
sa.PrimaryKeyConstraint('num')
62+
)
63+
op.create_table('main',
64+
sa.Column('num', sa.Integer(), autoincrement=True, nullable=False),
65+
sa.Column('balance', sa.String(), nullable=False),
66+
sa.Column('bananas', sa.Integer(), nullable=False),
67+
sa.Column('user_ID', sa.Integer(), nullable=False),
68+
sa.Column('immunity', sa.Integer(), nullable=False),
69+
sa.Column('level', sa.Integer(), nullable=False),
70+
sa.Column('inventory', sa.String(), nullable=True),
71+
sa.Column('winloss', sa.String(), nullable=True),
72+
sa.Column('invested', sa.String(), nullable=False),
73+
sa.PrimaryKeyConstraint('num')
74+
)
75+
op.create_index(op.f('ix_main_user_ID'), 'main', ['user_ID'], unique=True)
76+
op.create_table('messages',
77+
sa.Column('num', sa.Integer(), nullable=False),
78+
sa.Column('messageID', sa.Integer(), nullable=False),
79+
sa.Column('channelID', sa.Integer(), nullable=False),
80+
sa.Column('guildID', sa.Integer(), nullable=False),
81+
sa.PrimaryKeyConstraint('num')
82+
)
83+
op.create_table('misc',
84+
sa.Column('num', sa.Integer(), autoincrement=True, nullable=False),
85+
sa.Column('pointer', sa.String(), nullable=False),
86+
sa.Column('data', sa.Integer(), nullable=False),
87+
sa.PrimaryKeyConstraint('num')
88+
)
89+
op.create_table('osu',
90+
sa.Column('num', sa.Integer(), autoincrement=True, nullable=False),
91+
sa.Column('user_id', sa.Integer(), nullable=False),
92+
sa.Column('score', sa.Integer(), nullable=False),
93+
sa.Column('timestamp', sa.Integer(), nullable=False),
94+
sa.Column('amount', sa.Integer(), nullable=False),
95+
sa.Column('osu_user', sa.Integer(), nullable=False),
96+
sa.PrimaryKeyConstraint('num')
97+
)
98+
op.create_table('osu_users',
99+
sa.Column('num', sa.Integer(), autoincrement=True, nullable=False),
100+
sa.Column('osu_username', sa.String(), nullable=False),
101+
sa.Column('osu_id', sa.Integer(), nullable=False),
102+
sa.PrimaryKeyConstraint('num')
103+
)
104+
op.create_table('poll_state',
105+
sa.Column('message_id', sa.Integer(), nullable=False),
106+
sa.Column('options', sa.String(), nullable=False),
107+
sa.PrimaryKeyConstraint('message_id')
108+
)
109+
op.create_table('prestiege',
110+
sa.Column('num', sa.Integer(), autoincrement=True, nullable=False),
111+
sa.Column('user_id', sa.Integer(), nullable=False),
112+
sa.Column('pres1', sa.Integer(), nullable=True),
113+
sa.Column('pres2', sa.Integer(), nullable=True),
114+
sa.Column('pres3', sa.Integer(), nullable=True),
115+
sa.Column('pres4', sa.Integer(), nullable=True),
116+
sa.Column('pres5', sa.Integer(), nullable=True),
117+
sa.PrimaryKeyConstraint('num')
118+
)
119+
op.create_index(op.f('ix_prestiege_user_id'), 'prestiege', ['user_id'], unique=True)
120+
op.create_table('stocks',
121+
sa.Column('num', sa.Integer(), autoincrement=True, nullable=False),
122+
sa.Column('user_id', sa.Integer(), nullable=False),
123+
sa.Column('ticker', sa.String(), nullable=False),
124+
sa.Column('amount', sa.Integer(), nullable=False),
125+
sa.Column('purchase_price', sa.Integer(), nullable=False),
126+
sa.PrimaryKeyConstraint('num')
127+
)
128+
op.create_index(op.f('ix_stocks_user_id'), 'stocks', ['user_id'], unique=True)
129+
op.create_table('vote_multipliers',
130+
sa.Column('user_id', sa.Integer(), nullable=False),
131+
sa.Column('multiplier', sa.Integer(), nullable=False),
132+
sa.PrimaryKeyConstraint('user_id')
133+
)
134+
# ### end Alembic commands ###
135+
136+
137+
def downgrade() -> None:
138+
"""Downgrade schema."""
139+
# ### commands auto generated by Alembic - please adjust! ###
140+
op.drop_table('vote_multipliers')
141+
op.drop_index(op.f('ix_stocks_user_id'), table_name='stocks')
142+
op.drop_table('stocks')
143+
op.drop_index(op.f('ix_prestiege_user_id'), table_name='prestiege')
144+
op.drop_table('prestiege')
145+
op.drop_table('poll_state')
146+
op.drop_table('osu_users')
147+
op.drop_table('osu')
148+
op.drop_table('misc')
149+
op.drop_table('messages')
150+
op.drop_index(op.f('ix_main_user_ID'), table_name='main')
151+
op.drop_table('main')
152+
op.drop_table('ignore')
153+
op.drop_index(op.f('ix_history_user_id'), table_name='history')
154+
op.drop_table('history')
155+
op.drop_table('fire')
156+
op.drop_table('blacklist')
157+
# ### end Alembic commands ###
158+
159+
160+
# fmt: on

bot.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,11 @@
99
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
1010
from sqlalchemy.orm import sessionmaker
1111

12-
from models import Base
13-
1412
with open("config.toml", "rb") as f:
1513
config = tomllib.load(f)
1614

1715

1816
async def main():
19-
if not os.path.exists("data/database.sqlite"):
20-
os.makedirs("data", exist_ok=True)
21-
async with engine.begin() as conn:
22-
await conn.run_sync(Base.metadata.create_all)
23-
2417
# start the client
2518
async with client:
2619
for filename in os.listdir("./cogs"):

data/.gitkeep

Whitespace-only changes.

pyproject.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ dependencies = [
2525
"pyttsx3>=2.98",
2626
"discord-ext-menus",
2727
"anyio>=4.9.0",
28+
"alembic>=1.16.1",
2829
]
2930

3031
[dependency-groups]
@@ -39,6 +40,11 @@ package = false
3940
[tool.uv.sources]
4041
discord-ext-menus = { git = "https://github.com/Rapptz/discord-ext-menus" }
4142

43+
[tool.ruff]
44+
exclude = [
45+
"alembic/versions"
46+
]
47+
4248
[tool.ruff.lint]
4349
select = [
4450
"A004", # Check for import shadowing builtin functions: https://docs.astral.sh/ruff/rules/builtin-import-shadowing/

0 commit comments

Comments
 (0)