Skip to content

Commit 8cf39b9

Browse files
authored
Merge pull request #155 from ecmwf-projects/COPDS-2511-alembic
custom alembic cli for upgrade/downgrade
2 parents cf23e08 + 7e4b67e commit 8cf39b9

8 files changed

+85
-35
lines changed

README.md

+29-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Before pushing to GitHub, run the following commands:
2828
1. Run the static type checker: `make type-check`
2929
1. Build the documentation (see [Sphinx tutorial](https://www.sphinx-doc.org/en/master/tutorial/)): `make docs-build`
3030

31-
### Instructions for database updating
31+
### Instructions for creating a new database version
3232

3333
The package `cads-broker` comes with its 'broker' database.
3434
In case of database structure upgrade, developers must follow these steps:
@@ -46,6 +46,34 @@ In case of database structure upgrade, developers must follow these steps:
4646
Similarly, do the same with the `downgrade` function.
4747
1. Commit and push the modifications and the new file.
4848

49+
### Instructions for moving between different database versions
50+
51+
The package comes with its own 'broker-alembic-cli' script in order to move between different
52+
database versions. This script is a slight modified version of the 'alembic' script, overriding
53+
default config path used ([/alembic.ini](/alembic.ini)) and the sqlalchemy.url used, that is
54+
automatically computed by the environment and not read from any ini file.
55+
56+
All the database releases where you can migrate up and down must be defined by files contained inside
57+
the folder [/alembic/versions](/alembic/versions). All these files are in a version queue: each file has
58+
link to its revision hash (variable 'revision', the prefix of the file name) and to the next older one
59+
(variable 'down_revision'), and contains code to step up and down that database version.\
60+
Some useful commands are listed below.
61+
62+
- To migrate to the newest version, type:\
63+
`broker-alembic-cli upgrade head`
64+
- To upgrade to a specific version hash, for example 8ccbe515155c, type:\
65+
`broker-alembic-cli upgrade 8ccbe515155c`
66+
- To downgrade to a specific version hash, for example 8ccbe515155c, type:\
67+
`broker-alembic-cli downgrade 8ccbe515155c`
68+
- To get the current version hash of the database, type:\
69+
`broker-alembic-cli current`
70+
71+
Database migration changes could be applied to the cacholote component of the database, too. In such case,
72+
migrate the cacholote component after the migration by the 'broker-alembic-cli' tool.
73+
74+
Other details are the same of the standard alembic migration tool,
75+
see the [Alembic tutorial](https://alembic.sqlalchemy.org/en/latest/tutorial.html).
76+
4977
For details about the alembic migration tool, see the [Alembic tutorial](https://alembic.sqlalchemy.org/en/latest/tutorial.html).
5078

5179
## Quality of Service rules examples

alembic.ini

+1-7
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[alembic]
44
# path to migration scripts
5-
script_location = alembic
5+
script_location = %(here)s/alembic
66

77
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
88
# Uncomment the line below if you want the files to be prepended with date and time
@@ -60,12 +60,6 @@ version_path_separator = os # Use os.pathsep. Default configuration used for ne
6060
# are written from script.py.mako
6161
# output_encoding = utf-8
6262

63-
drivername =
64-
username =
65-
password =
66-
host =
67-
port =
68-
database =
6963
sqlalchemy.url = %(drivername)s://%(username)s:%(password)s@%(host)s:%(port)s/%(database)s
7064

7165
[post_write_hooks]

alembic/env.py

+3-13
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@
1919
import alembic.context
2020
import cads_broker
2121

22-
config = alembic.context.config
23-
2422

2523
def run_migrations_offline() -> None:
2624
"""Run migrations in 'offline' mode.
@@ -33,11 +31,7 @@ def run_migrations_offline() -> None:
3331
Calls to alembic.context.execute() here emit the given string to the
3432
script output.
3533
"""
36-
url_props = dict()
37-
for prop in ["drivername", "username", "password", "host", "port", "database"]:
38-
url_props[prop] = config.get_main_option(prop)
39-
url_props["port"] = url_props["port"] and int(url_props["port"]) or None # type: ignore
40-
url = sa.engine.URL.create(**url_props) # type: ignore
34+
url = alembic.context.config.get_main_option("sqlalchemy.url")
4135
alembic.context.configure(
4236
url=url,
4337
target_metadata=cads_broker.database.BaseModel.metadata,
@@ -54,12 +48,8 @@ def run_migrations_online() -> None:
5448
In this scenario we need to create an Engine
5549
and associate a connection with the alembic.context.
5650
"""
57-
url_props = dict()
58-
for prop in ["drivername", "username", "password", "host", "port", "database"]:
59-
url_props[prop] = config.get_main_option(prop)
60-
url_props["port"] = url_props["port"] and int(url_props["port"]) or None # type: ignore
61-
url = sa.engine.URL.create(**url_props) # type: ignore
62-
engine = sa.create_engine(url, poolclass=sa.pool.NullPool)
51+
url = alembic.context.config.get_main_option("sqlalchemy.url")
52+
engine = sa.create_engine(url, poolclass=sa.pool.NullPool) # type: ignore
6353
with engine.connect() as connection:
6454
alembic.context.configure(
6555
connection=connection,

alembic/versions/a4e8be715296_add_deleted_as_new_status.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,9 @@ def upgrade() -> None:
4646
def downgrade() -> None:
4747
# Remove the new status from the enum
4848
# this doesn't work
49-
op.execute("ALTER TYPE status DELETE VALUE 'deleted'")
49+
#op.execute("ALTER TYPE status DELETE VALUE 'deleted'")
50+
op.execute("CREATE TYPE status_old AS ENUM ('accepted','running','failed','successful','dismissed')")
51+
op.execute("DELETE FROM system_requests where status='deleted'")
52+
op.execute("ALTER TABLE system_requests ALTER COLUMN status TYPE status_old USING (status::text::status_old)")
53+
op.execute("DROP TYPE status")
54+
op.execute("ALTER TYPE status_old RENAME TO status")

alembic/versions/d5d4afc97d40_more_indexes_on_system_requests.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ def upgrade() -> None:
2626

2727

2828
def downgrade() -> None:
29-
alembic.op.drop_index("idx_system_requests_status")
30-
alembic.op.drop_index("idx_system_requests_created_at")
31-
alembic.op.drop_index("idx_system_requests_finished_at")
29+
alembic.op.drop_index("idx_system_requests_status", if_exists=True)
30+
alembic.op.drop_index("idx_system_requests_created_at", if_exists=True)
31+
alembic.op.drop_index("idx_system_requests_finished_at", if_exists=True)
32+
alembic.op.drop_index("ix_system_requests_status", if_exists=True)
33+
alembic.op.drop_index("ix_system_requests_created_at", if_exists=True)
34+
alembic.op.drop_index("ix_system_requests_finished_at", if_exists=True)

cads_broker/alembic_cli.py

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"""Custom alembic CLI with new default config path + db url by environment."""
2+
3+
import os
4+
from typing import Optional, Sequence
5+
6+
import cads_broker.config
7+
from alembic.config import CommandLine, Config
8+
9+
alembic_ini_path = os.path.abspath(os.path.join(__file__, "..", "..", "alembic.ini"))
10+
11+
12+
class MyCommandLine(CommandLine):
13+
def main(self, argv: Optional[Sequence[str]] = None) -> None:
14+
options = self.parser.parse_args(argv)
15+
if not hasattr(options, "cmd"):
16+
self.parser.error("too few arguments")
17+
else:
18+
cfg = Config(
19+
file_=options.config,
20+
ini_section=options.name,
21+
cmd_opts=options,
22+
)
23+
url = cads_broker.config.ensure_settings().connection_string.replace(
24+
"%", "%%"
25+
)
26+
cfg.set_main_option("sqlalchemy.url", url)
27+
self.run_cmd(cfg, options)
28+
29+
30+
def main() -> None:
31+
cli = MyCommandLine(prog="broker-alembic-cli")
32+
config_in_parser = [p for p in cli.parser._actions if p.dest == "config"][0]
33+
config_in_parser.default = alembic_ini_path
34+
config_in_parser.help = f'Alternate config file; defaults to "{alembic_ini_path}"'
35+
cli.main()

cads_broker/database.py

+4-10
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
import alembic.command
2121
import alembic.config
22-
from cads_broker import config
22+
from cads_broker import alembic_cli, config
2323

2424
BaseModel = sa.orm.declarative_base()
2525

@@ -932,15 +932,9 @@ def init_database(connection_string: str, force: bool = False) -> sa.engine.Engi
932932
:param force: if True, drop the database structure and build again from scratch
933933
"""
934934
engine = sa.create_engine(connection_string)
935-
migration_directory = os.path.abspath(os.path.join(__file__, "..", ".."))
936-
os.chdir(migration_directory)
937-
alembic_config_path = os.path.join(migration_directory, "alembic.ini")
938-
alembic_cfg = alembic.config.Config(alembic_config_path)
939-
for option in ["drivername", "username", "password", "host", "port", "database"]:
940-
value = getattr(engine.url, option)
941-
if value is None:
942-
value = ""
943-
alembic_cfg.set_main_option(option, str(value))
935+
alembic_cfg = alembic.config.Config(alembic_cli.alembic_ini_path)
936+
# passwords with special chars are urlencoded, but '%' must be escaped in ini files
937+
alembic_cfg.set_main_option("sqlalchemy.url", connection_string.replace("%", "%%"))
944938
if not sqlalchemy_utils.database_exists(engine.url):
945939
sqlalchemy_utils.create_database(engine.url)
946940
# cleanup and create the schema

pyproject.toml

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ readme = "README.md"
3131

3232
[project.scripts]
3333
broker = "cads_broker.entry_points:main"
34+
broker-alembic-cli = "cads_broker.alembic_cli:main"
3435

3536
[tool.coverage.run]
3637
branch = true

0 commit comments

Comments
 (0)