Skip to content

Commit b62982b

Browse files
committed
Add Alembic example files
1 parent 5c1ef83 commit b62982b

File tree

3 files changed

+295
-0
lines changed

3 files changed

+295
-0
lines changed

docs/manager/alembic/alembic.ini

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# a multi-database configuration.
2+
3+
[alembic]
4+
# path to migration scripts
5+
script_location = 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%%(second).2d-%%(rev)s_%%(slug)s
12+
13+
# sys.path path, will be prepended to sys.path if present.
14+
# defaults to the current working directory.
15+
prepend_sys_path = .
16+
17+
# timezone to use when rendering the date within the migration file
18+
# as well as the filename.
19+
# If specified, requires the python-dateutil library that can be
20+
# installed by adding `alembic[tz]` to the pip requirements
21+
# string value is passed to dateutil.tz.gettz()
22+
# leave blank for localtime
23+
# timezone =
24+
25+
# max length of characters to apply to the
26+
# "slug" field
27+
# truncate_slug_length = 40
28+
29+
# set to 'true' to run the environment during
30+
# the 'revision' command, regardless of autogenerate
31+
# revision_environment = false
32+
33+
# set to 'true' to allow .pyc and .pyo files without
34+
# a source .py file to be detected as revisions in the
35+
# versions/ directory
36+
# sourceless = false
37+
38+
# version location specification; This defaults
39+
# to alembic/versions. When using multiple version
40+
# directories, initial revisions must be specified with --version-path.
41+
# The path separator used here should be the separator specified by "version_path_separator" below.
42+
# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions
43+
44+
# version path separator; As mentioned above, this is the character used to split
45+
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
46+
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
47+
# Valid values for version_path_separator are:
48+
#
49+
# version_path_separator = :
50+
# version_path_separator = ;
51+
# version_path_separator = space
52+
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
53+
54+
# the output encoding used when revision files
55+
# are written from script.py.mako
56+
# output_encoding = utf-8
57+
58+
# We inject db names and config using env.py in alembic directory
59+
#databases = engine1, engine2
60+
61+
#[engine1]
62+
#sqlalchemy.url = driver://user:pass@localhost/dbname
63+
64+
#[engine2]
65+
#sqlalchemy.url = driver://user:pass@localhost/dbname2
66+
67+
[post_write_hooks]
68+
# post_write_hooks defines scripts or Python functions that are run
69+
# on newly generated revision scripts. See the documentation for further
70+
# detail and examples
71+
72+
# format using "black" - use the console_scripts runner, against the "black" entrypoint
73+
# hooks = black
74+
# black.type = console_scripts
75+
# black.entrypoint = black
76+
# black.options = -l 79 REVISION_SCRIPT_FILENAME

docs/manager/alembic/env.py

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import logging
2+
import os
3+
from asyncio import get_event_loop
4+
5+
from alembic import context
6+
from sqlalchemy import Column, Integer, String
7+
from sqlalchemy.ext.asyncio import AsyncEngine
8+
from sqlalchemy_bind_manager import SQLAlchemyAsyncConfig, SQLAlchemyBindManager
9+
10+
################################################################
11+
## Note: The bind_config, sa_manager and models are normally ##
12+
## implemented in an application. This is only an example! ##
13+
################################################################
14+
bind_config = {
15+
"default": SQLAlchemyAsyncConfig(
16+
engine_url=f"sqlite+aiosqlite:///{os.path.dirname(os.path.abspath(__file__))}/sqlite.db",
17+
engine_options=dict(
18+
connect_args={
19+
"check_same_thread": False,
20+
},
21+
echo=False,
22+
future=True,
23+
),
24+
),
25+
}
26+
27+
sa_manager = SQLAlchemyBindManager(config=bind_config)
28+
29+
class BookModel(sa_manager.get_bind().model_declarative_base):
30+
id = Column(Integer)
31+
title = Column(String)
32+
################################################################
33+
## Note: The bind_config, sa_manager and models are normally ##
34+
## implemented in an application. This is only an example! ##
35+
################################################################
36+
37+
38+
USE_TWOPHASE = False
39+
40+
# this is the Alembic Config object, which provides
41+
# access to the values within the .ini file in use.
42+
config = context.config
43+
44+
logger = logging.getLogger("alembic.env")
45+
target_metadata = sa_manager.get_bind_mappers_metadata()
46+
db_names = target_metadata.keys()
47+
config.set_main_option("databases", ",".join(db_names))
48+
49+
50+
def run_migrations_offline() -> None:
51+
"""Run migrations in 'offline' mode.
52+
53+
This configures the context with just a URL
54+
and not an Engine, though an Engine is acceptable
55+
here as well. By skipping the Engine creation
56+
we don't even need a DBAPI to be available.
57+
58+
Calls to context.execute() here emit the given string to the
59+
script output.
60+
61+
"""
62+
# for the --sql use case, run migrations for each URL into
63+
# individual files.
64+
65+
engines = {}
66+
for name in db_names:
67+
engines[name] = {}
68+
engines[name]["url"] = sa_manager.get_bind(name).engine.url
69+
70+
for name, rec in engines.items():
71+
logger.info(f"Migrating database {name}")
72+
file_ = f"{name}.sql"
73+
logger.info(f"Writing output to {file_}")
74+
with open(file_, "w") as buffer:
75+
context.configure(
76+
url=rec["url"],
77+
output_buffer=buffer,
78+
target_metadata=target_metadata.get(name),
79+
literal_binds=True,
80+
dialect_opts={"paramstyle": "named"},
81+
)
82+
with context.begin_transaction():
83+
context.run_migrations(engine_name=name)
84+
85+
86+
def do_run_migration(conn, name):
87+
context.configure(
88+
connection=conn,
89+
upgrade_token=f"{name}_upgrades",
90+
downgrade_token=f"{name}_downgrades",
91+
target_metadata=target_metadata.get(name),
92+
)
93+
context.run_migrations(engine_name=name)
94+
95+
96+
async def run_migrations_online() -> None:
97+
"""Run migrations in 'online' mode.
98+
99+
In this scenario we need to create an Engine
100+
and associate a connection with the context.
101+
"""
102+
103+
# for the direct-to-DB use case, start a transaction on all
104+
# engines, then run all migrations, then commit all transactions.
105+
106+
engines = {}
107+
for name in db_names:
108+
engines[name] = {}
109+
engines[name]["engine"] = sa_manager.get_bind(name).engine
110+
111+
for name, rec in engines.items():
112+
engine = rec["engine"]
113+
if isinstance(engine, AsyncEngine):
114+
rec["connection"] = conn = await engine.connect()
115+
116+
if USE_TWOPHASE:
117+
rec["transaction"] = await conn.begin_twophase()
118+
else:
119+
rec["transaction"] = await conn.begin()
120+
else:
121+
rec["connection"] = conn = engine.connect()
122+
123+
if USE_TWOPHASE:
124+
rec["transaction"] = conn.begin_twophase()
125+
else:
126+
rec["transaction"] = conn.begin()
127+
128+
try:
129+
for name, rec in engines.items():
130+
logger.info(f"Migrating database {name}")
131+
if isinstance(rec["engine"], AsyncEngine):
132+
133+
def migration_callable(*args, **kwargs):
134+
return do_run_migration(*args, name=name, **kwargs)
135+
136+
await rec["connection"].run_sync(migration_callable)
137+
else:
138+
do_run_migration(name, rec)
139+
140+
if USE_TWOPHASE:
141+
for rec in engines.values():
142+
if isinstance(rec["engine"], AsyncEngine):
143+
await rec["transaction"].prepare()
144+
else:
145+
rec["transaction"].prepare()
146+
147+
for rec in engines.values():
148+
if isinstance(rec["engine"], AsyncEngine):
149+
await rec["transaction"].commit()
150+
else:
151+
rec["transaction"].commit()
152+
except:
153+
for rec in engines.values():
154+
if isinstance(rec["engine"], AsyncEngine):
155+
await rec["transaction"].rollback()
156+
else:
157+
rec["transaction"].rollback()
158+
raise
159+
finally:
160+
for rec in engines.values():
161+
if isinstance(rec["engine"], AsyncEngine):
162+
await rec["connection"].close()
163+
else:
164+
rec["connection"].close()
165+
166+
167+
if context.is_offline_mode():
168+
run_migrations_offline()
169+
else:
170+
loop = get_event_loop()
171+
if loop.is_running():
172+
loop.create_task(run_migrations_online())
173+
else:
174+
loop.run_until_complete(run_migrations_online())
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<%!
2+
import re
3+
4+
%>"""${message}
5+
6+
Revision ID: ${up_revision}
7+
Revises: ${down_revision | comma,n}
8+
Create Date: ${create_date}
9+
10+
"""
11+
from alembic import op
12+
import sqlalchemy as sa
13+
${imports if imports else ""}
14+
15+
# revision identifiers, used by Alembic.
16+
revision = ${repr(up_revision)}
17+
down_revision = ${repr(down_revision)}
18+
branch_labels = ${repr(branch_labels)}
19+
depends_on = ${repr(depends_on)}
20+
21+
22+
def upgrade(engine_name: str) -> None:
23+
globals()[f"upgrade_{engine_name}"]()
24+
25+
26+
def downgrade(engine_name: str) -> None:
27+
globals()[f"downgrade_{engine_name}"]()
28+
29+
<%
30+
db_names = config.get_main_option("databases")
31+
%>
32+
33+
## generate an "upgrade_<xyz>() / downgrade_<xyz>()" function
34+
## for each database name in the ini file.
35+
36+
% for db_name in re.split(r',\s*', db_names):
37+
38+
def upgrade_${db_name}() -> None:
39+
${context.get(f"{db_name}_upgrades", "pass")}
40+
41+
42+
def downgrade_${db_name}() -> None:
43+
${context.get(f"{db_name}_downgrades", "pass")}
44+
45+
% endfor

0 commit comments

Comments
 (0)