-
Notifications
You must be signed in to change notification settings - Fork 451
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 #7622 from drew2a/feature/triblerdb_migration
Add `TriblerDatabaseMigrationChain`
- Loading branch information
Showing
12 changed files
with
353 additions
and
10 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
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,18 @@ | ||
import pytest | ||
|
||
from tribler.core.components.database.db.tribler_database import TriblerDatabase | ||
from tribler.core.upgrade.tribler_db.migration_chain import TriblerDatabaseMigrationChain | ||
from tribler.core.utilities.path_util import Path | ||
from tribler.core.utilities.simpledefs import STATEDIR_DB_DIR | ||
|
||
|
||
# pylint: disable=redefined-outer-name | ||
|
||
|
||
@pytest.fixture | ||
def migration_chain(tmpdir): | ||
""" Create an empty migration chain with an empty database.""" | ||
db_file_name = Path(tmpdir) / STATEDIR_DB_DIR / 'tribler.db' | ||
db_file_name.parent.mkdir() | ||
TriblerDatabase(filename=str(db_file_name)) | ||
return TriblerDatabaseMigrationChain(state_dir=Path(tmpdir), chain=[]) |
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,53 @@ | ||
import functools | ||
import logging | ||
from typing import Callable, Optional | ||
|
||
from tribler.core.components.database.db.tribler_database import TriblerDatabase | ||
from tribler.core.utilities.pony_utils import db_session | ||
|
||
MIGRATION_METADATA = "_tribler_db_migration" | ||
|
||
logger = logging.getLogger('Migration (TriblerDB)') | ||
|
||
|
||
def migration(execute_only_if_version: int, set_after_successful_execution_version: Optional[int] = None): | ||
""" Decorator for migration functions. | ||
The migration executes in the single transaction. If the migration fails, the transaction is rolled back. | ||
The decorator also sets the metadata attribute to the decorated function. It could be checked by | ||
calling the `has_migration_metadata` function. | ||
Args: | ||
execute_only_if_version: Execute the migration only if the current db version is equal to this value. | ||
set_after_successful_execution_version: Set the db version to this value after the migration is executed. | ||
If it is not specified, then `set_after_successful_execution_version = execute_only_if_version + 1` | ||
""" | ||
|
||
def decorator(func): | ||
@functools.wraps(func) | ||
@db_session | ||
def wrapper(db: TriblerDatabase, **kwargs): | ||
target_version = execute_only_if_version | ||
if target_version != db.version: | ||
logger.info( | ||
f"Function {func.__name__} is not executed because DB version is not equal to {target_version}. " | ||
f"The current db version is {db.version}" | ||
) | ||
return None | ||
|
||
result = func(db, **kwargs) | ||
|
||
next_version = set_after_successful_execution_version | ||
if next_version is None: | ||
next_version = target_version + 1 | ||
db.version = next_version | ||
|
||
return result | ||
|
||
setattr(wrapper, MIGRATION_METADATA, {}) | ||
return wrapper | ||
|
||
return decorator | ||
|
||
|
||
def has_migration_metadata(f: Callable): | ||
""" Check if the function has migration metadata.""" | ||
return hasattr(f, MIGRATION_METADATA) |
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,48 @@ | ||
import logging | ||
from typing import Callable, List, Optional | ||
|
||
from tribler.core.components.database.db.tribler_database import TriblerDatabase | ||
from tribler.core.upgrade.tribler_db.decorator import has_migration_metadata | ||
from tribler.core.upgrade.tribler_db.scheme_migrations.scheme_migration_0 import scheme_migration_0 | ||
from tribler.core.utilities.path_util import Path | ||
from tribler.core.utilities.simpledefs import STATEDIR_DB_DIR | ||
|
||
|
||
class TriblerDatabaseMigrationChain: | ||
""" A chain of migrations that can be executed on a TriblerDatabase. | ||
To create a new migration, create a new function and decorate it with the `migration` decorator. Then add it to | ||
the `DEFAULT_CHAIN` list. | ||
""" | ||
|
||
DEFAULT_CHAIN = [ | ||
scheme_migration_0, | ||
# add your migration here | ||
] | ||
|
||
def __init__(self, state_dir: Path, chain: Optional[List[Callable]] = None): | ||
self.logger = logging.getLogger(self.__class__.__name__) | ||
self.state_dir = state_dir | ||
|
||
db_path = self.state_dir / STATEDIR_DB_DIR / 'tribler.db' | ||
self.logger.info(f'Tribler DB path: {db_path}') | ||
self.db = TriblerDatabase(str(db_path), check_tables=False) if db_path.is_file() else None | ||
|
||
self.migrations = chain or self.DEFAULT_CHAIN | ||
|
||
def execute(self) -> bool: | ||
""" Execute all migrations in the chain. | ||
Returns: True if all migrations were executed successfully, False otherwise. | ||
An exception in any of the migrations will halt the execution chain and be re-raised. | ||
""" | ||
|
||
if not self.db: | ||
return False | ||
|
||
for m in self.migrations: | ||
if not has_migration_metadata(m): | ||
raise NotImplementedError(f'The migration {m} should have `migration` decorator') | ||
m(self.db, state_dir=self.state_dir) | ||
|
||
return True |
23 changes: 23 additions & 0 deletions
23
src/tribler/core/upgrade/tribler_db/scheme_migrations/scheme_migration_0.py
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,23 @@ | ||
from tribler.core.components.database.db.tribler_database import TriblerDatabase | ||
from tribler.core.upgrade.tribler_db.decorator import migration | ||
|
||
|
||
@migration(execute_only_if_version=0) | ||
def scheme_migration_0(db: TriblerDatabase, **kwargs): # pylint: disable=unused-argument | ||
""" "This is initial migration, placed here primarily for demonstration purposes. | ||
It doesn't do anything except set the database version to `1`. | ||
For upcoming migrations, there are some guidelines: | ||
1. functions should contain a single parameter, `db: TriblerDatabase`, | ||
2. they should apply the `@migration` decorator. | ||
Utilizing plain SQL (as seen in the example below) is considered good practice since it helps prevent potential | ||
inconsistencies in DB schemes in the future (model versions preceding the current one may differ from it). | ||
For more information see: https://github.com/Tribler/tribler/issues/7382 | ||
The example of a migration: | ||
db.execute('ALTER TABLE "TorrentState" ADD "has_data" BOOLEAN DEFAULT 0') | ||
db.execute('UPDATE "TorrentState" SET "has_data" = 1 WHERE last_check > 0') | ||
""" |
13 changes: 13 additions & 0 deletions
13
src/tribler/core/upgrade/tribler_db/scheme_migrations/tests/test_scheme_migration_0.py
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,13 @@ | ||
from tribler.core.upgrade.tribler_db.migration_chain import TriblerDatabaseMigrationChain | ||
from tribler.core.upgrade.tribler_db.scheme_migrations.scheme_migration_0 import scheme_migration_0 | ||
from tribler.core.utilities.pony_utils import db_session | ||
|
||
|
||
@db_session | ||
def test_scheme_migration_0(migration_chain: TriblerDatabaseMigrationChain): | ||
""" Test that the scheme_migration_0 changes the database version to 1. """ | ||
migration_chain.db.version = 0 | ||
migration_chain.migrations = [scheme_migration_0] | ||
|
||
assert migration_chain.execute() | ||
assert migration_chain.db.version == 1 |
74 changes: 74 additions & 0 deletions
74
src/tribler/core/upgrade/tribler_db/tests/test_decorator.py
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,74 @@ | ||
from unittest.mock import Mock | ||
|
||
import pytest | ||
|
||
from tribler.core.upgrade.tribler_db.decorator import has_migration_metadata, migration | ||
|
||
|
||
def test_migration_execute_only_if_version(): | ||
""" Test that migration is executed only if the version of the database is equal to the specified one.""" | ||
|
||
@migration(execute_only_if_version=1) | ||
def test(_: Mock): | ||
return True | ||
|
||
assert test(Mock(version=1)) | ||
assert not test(Mock(version=2)) | ||
|
||
|
||
def test_set_after_successful_execution_version(): | ||
""" Test that the version of the database is set to the specified one after the migration is successfully | ||
executed. | ||
""" | ||
|
||
@migration(execute_only_if_version=1, set_after_successful_execution_version=33) | ||
def test(_: Mock): | ||
... | ||
|
||
db = Mock(version=1) | ||
test(db) | ||
|
||
assert db.version == 33 | ||
|
||
|
||
def test_set_after_successful_execution_version_not_specified(): | ||
""" Test that if the version is not specified, the version of the database will be set to | ||
execute_only_if_version + 1 | ||
""" | ||
|
||
@migration(execute_only_if_version=1) | ||
def test(_: Mock): | ||
... | ||
|
||
db = Mock(version=1) | ||
test(db) | ||
|
||
assert db.version == 2 | ||
|
||
|
||
def test_set_after_successful_execution_raise_an_exception(): | ||
""" Test that if an exception is raised during the migration, the version of the database is not changed.""" | ||
|
||
@migration(execute_only_if_version=1, set_after_successful_execution_version=33) | ||
def test(_: Mock): | ||
raise TypeError | ||
|
||
db = Mock(version=1) | ||
with pytest.raises(TypeError): | ||
test(db) | ||
|
||
assert db.version == 1 | ||
|
||
|
||
def test_set_metadata(): | ||
""" Test that the metadata flag is set.""" | ||
|
||
@migration(execute_only_if_version=1) | ||
def simple_migration(_: Mock): | ||
... | ||
|
||
def no_migration(_: Mock): | ||
... | ||
|
||
assert has_migration_metadata(simple_migration) | ||
assert not has_migration_metadata(no_migration) |
Oops, something went wrong.