-
Notifications
You must be signed in to change notification settings - Fork 81
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #304 from DHI-GRAS/migration
Add bare-bones migration script
- Loading branch information
Showing
10 changed files
with
206 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
"""migrations/__init__.py | ||
Define available migrations. | ||
""" | ||
|
||
import os | ||
import glob | ||
import importlib | ||
|
||
|
||
MIGRATIONS = {} | ||
|
||
glob_pattern = os.path.join(os.path.dirname(__file__), "v*_*.py") | ||
|
||
for modpath in glob.glob(glob_pattern): | ||
modname = os.path.basename(modpath)[: -len(".py")] | ||
mod = importlib.import_module(f"{__name__}.{modname}") | ||
assert all( | ||
hasattr(mod, attr) for attr in ("up_version", "down_version", "upgrade_sql") | ||
) | ||
assert mod.down_version not in MIGRATIONS | ||
MIGRATIONS[mod.down_version] = mod |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
up_version = (0, 8) | ||
down_version = (0, 7) | ||
|
||
upgrade_sql = [ | ||
"CREATE TABLE key_names (key_name TEXT PRIMARY KEY, description TEXT, idx INTEGER UNIQUE)", | ||
"INSERT INTO key_names (key_name, description, idx) SELECT key, description, row_number() over (order by (select NULL)) FROM keys", | ||
"UPDATE terracotta SET version='0.8.0'", | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
"""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 | ||
from terracotta.drivers.relational_meta_store import RelationalMetaStore | ||
|
||
|
||
def parse_version(verstr: str) -> Tuple[int, ...]: | ||
"""Convert 'v<major>.<minor>.<patch>' 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.option("-y", "--yes", is_flag=True, help="Do not ask for confirmation.") | ||
@click.command("migrate") | ||
def migrate(database: str, to_version: str, from_version: str, yes: bool) -> None: | ||
from_version_tuple, to_version_tuple, tc_version_tuple = ( | ||
parse_version(v)[:2] if v is not None else None | ||
for v in (from_version, to_version, __version__) | ||
) | ||
|
||
driver = get_driver(database) | ||
meta_store = driver.meta_store | ||
assert isinstance(meta_store, RelationalMetaStore) | ||
|
||
if to_version_tuple > tc_version_tuple: | ||
raise ValueError( | ||
f"Unknown target version {join_version(to_version_tuple)} (this is {join_version(tc_version_tuple)}). Try upgrading terracotta." | ||
) | ||
|
||
if from_version_tuple is None: | ||
try: # type: ignore | ||
with meta_store.connect(verify=False): | ||
from_version_tuple = parse_version(driver.db_version)[:2] | ||
except Exception as e: | ||
raise RuntimeError("Cannot determine database version.") from e | ||
|
||
if from_version_tuple == to_version_tuple: | ||
click.echo("Already at target version, nothing to do.") | ||
return | ||
|
||
migration_chain = [] | ||
current_version = from_version_tuple | ||
|
||
while current_version != to_version_tuple: | ||
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_tuple)} -> {join_version(to_version_tuple)} and execute the above SQL commands." | ||
) | ||
|
||
if not yes: | ||
click.confirm("Continue?", abort=True) | ||
|
||
with meta_store.connect(verify=False) as conn: | ||
for migration in migration_chain: | ||
for cmd in migration.upgrade_sql: | ||
conn.execute(sqla.text(cmd)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
from click.testing import CliRunner | ||
|
||
|
||
def parse_version(verstr): | ||
"""Convert 'v<major>.<minor>.<patch>' to (major, minor, patch)""" | ||
components = verstr.split(".") | ||
components[0] = components[0].lstrip("v") | ||
return tuple(int(c) for c in components[:3]) | ||
|
||
|
||
def test_migrate(v07_db, monkeypatch, force_reload): | ||
"""Test database migration to next major version if one is available.""" | ||
with monkeypatch.context() as m: | ||
# pretend we are at the next major version | ||
import terracotta | ||
|
||
current_version = parse_version(terracotta.__version__) | ||
next_major_version = (current_version[0], current_version[1] + 1, 0) | ||
m.setattr(terracotta, "__version__", ".".join(map(str, next_major_version))) | ||
|
||
# run migration | ||
from terracotta import get_driver | ||
from terracotta.scripts import cli | ||
from terracotta.migrations import MIGRATIONS | ||
|
||
runner = CliRunner() | ||
result = runner.invoke( | ||
cli.cli, ["migrate", str(v07_db), "--from", "v0.7", "--yes"] | ||
) | ||
assert result.exit_code == 0 | ||
|
||
if next_major_version[:2] not in [m.up_version for m in MIGRATIONS.values()]: | ||
assert "Unknown target version" in result.output | ||
return | ||
|
||
assert "Upgrade path found" in result.output | ||
|
||
driver_updated = get_driver(str(v07_db), provider="sqlite") | ||
assert driver_updated.key_names == ("key1", "akey", "key2") |