From 3ec39e928d046fffd2f35643fa00174c35c1c849 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dion=20H=C3=A4fner?= Date: Sat, 27 Aug 2022 22:39:13 +0200 Subject: [PATCH] first prototype of migration script (#245) --- terracotta/migrations/__init__.py | 17 ++++++++ terracotta/migrations/v0_8.py | 13 ++++++ terracotta/scripts/cli.py | 2 + terracotta/scripts/migrate.py | 72 +++++++++++++++++++++++++++++++ 4 files changed, 104 insertions(+) create mode 100644 terracotta/migrations/__init__.py create mode 100644 terracotta/migrations/v0_8.py create mode 100644 terracotta/scripts/migrate.py diff --git a/terracotta/migrations/__init__.py b/terracotta/migrations/__init__.py new file mode 100644 index 00000000..f9210dfd --- /dev/null +++ b/terracotta/migrations/__init__.py @@ -0,0 +1,17 @@ +"""migrations/__init__.py + +Define available migrations. +""" + +import os +import glob +import importlib + + +MIGRATIONS = {} + +for modname in glob.glob("v*_*.py", root_dir=os.path.dirname(__file__)): + mod = importlib.import_module(f"{__name__}.{modname[:-3]}") + assert all(hasattr(mod, attr) for attr in ("up_version", "down_version", "upgrade_sql", "downgrade_sql")) + assert mod.down_version not in MIGRATIONS + MIGRATIONS[mod.down_version] = mod \ No newline at end of file diff --git a/terracotta/migrations/v0_8.py b/terracotta/migrations/v0_8.py new file mode 100644 index 00000000..1995e21b --- /dev/null +++ b/terracotta/migrations/v0_8.py @@ -0,0 +1,13 @@ + + +up_version = (0, 8) +down_version = (0, 7) + +upgrade_sql = [ + # "CREATE TABLE", + # "ALTER TABLE keys ADD COLUMN index INTEGER UNIQUE", + # "ALTER TABLE keys RENAME TO key_names", + "UPDATE terracotta SET version='v0.8.0'", +] + +downgrade_sql = [] \ No newline at end of file diff --git a/terracotta/scripts/cli.py b/terracotta/scripts/cli.py index be61aa9a..0dab7e08 100644 --- a/terracotta/scripts/cli.py +++ b/terracotta/scripts/cli.py @@ -69,6 +69,8 @@ def entrypoint() -> None: from terracotta.scripts.serve import serve cli.add_command(serve) +from terracotta.scripts.migrate import migrate +cli.add_command(migrate) if __name__ == '__main__': entrypoint() diff --git a/terracotta/scripts/migrate.py b/terracotta/scripts/migrate.py new file mode 100644 index 00000000..34784925 --- /dev/null +++ b/terracotta/scripts/migrate.py @@ -0,0 +1,72 @@ +"""scripts/migrate.py + +Migrate databases between Terracotta versions. +""" + +from typing import Tuple + +import click +import sqlalchemy as sqla + +from terracotta import get_driver, __version__ +from terracotta.migrations import MIGRATIONS + + +def parse_version(verstr: str) -> Tuple[int]: + """Convert 'v..' to (major, minor, patch)""" + components = verstr.split(".") + components[0] = components[0].lstrip("v") + return tuple(int(c) for c in components[:3]) + + +def join_version(vertuple: Tuple[int]) -> str: + return "v" + ".".join(map(str, vertuple)) + + +@click.argument("DATABASE", required=True) +@click.option("--from", "from_version", required=False, default=None) +@click.option("--to", "to_version", required=False, default=__version__) +@click.command("migrate") +def migrate(database: str, to_version: str, from_version: str): + from_version, to_version, tc_version = (parse_version(v)[:2] if v else None for v in (from_version, to_version, __version__)) + + driver = get_driver(database) + + # if to_version > tc_version: + # raise ValueError(f"Unknown target version {join_version(to_version)} (this is {join_version(tc_version)}). Try upgrading terracotta.") + + if from_version is None: + try: + with driver.connect(verify=False): + from_version = parse_version(driver.db_version)[:2] + except: + raise RuntimeError("Cannot determine database version.") + + migration_chain = [] + current_version = from_version + + while current_version != to_version: + if current_version not in MIGRATIONS: + raise RuntimeError("Unexpected error") + + migration = MIGRATIONS[current_version] + migration_chain.append(migration) + current_version = migration.up_version + + click.echo("Upgrade path found\n") + + for migration in migration_chain: + click.echo(f"{join_version(migration.down_version)} -> {join_version(migration.up_version)}") + + for cmd in migration.upgrade_sql: + click.echo(f" {cmd}") + + click.echo("") + + click.echo(f"This will upgrade the database from {join_version(from_version)} -> {join_version(to_version)} and execute the above SQL commands.") + click.confirm("Continue?", abort=True) + + with driver.connect(verify=False): + for migration in migration_chain: + for cmd in migration.upgrade_sql: + driver.meta_store.connection.execute(sqla.text(cmd)) \ No newline at end of file