-
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.
- Loading branch information
Showing
12 changed files
with
399 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: Optional[int] = None, | ||
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. | ||
""" | ||
|
||
def decorator(func): | ||
@functools.wraps(func) | ||
@db_session | ||
def wrapper(db: TriblerDatabase, **kwargs): | ||
if (version := execute_only_if_version) is not None: | ||
if version != db.version: | ||
logger.info( | ||
f"Function {func.__name__} is not executed because DB version is not equal to {version}. " | ||
f"The current db version is {db.version}" | ||
) | ||
return None | ||
|
||
result = func(db, **kwargs) | ||
|
||
if (version := set_after_successful_execution_version) is not None: | ||
db.version = 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,53 @@ | ||
import logging | ||
from typing import Callable, Iterator, 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 _ in self.steps(): | ||
... | ||
|
||
return True | ||
|
||
def steps(self) -> Iterator: | ||
""" Execute migrations step by step.""" | ||
for m in self.migrations: | ||
if not has_migration_metadata(m): | ||
raise NotImplementedError(f'The migration {m} should have `migration` decorator') | ||
yield m(self.db, state_dir=self.state_dir) |
26 changes: 26 additions & 0 deletions
26
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,26 @@ | ||
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, | ||
set_after_successful_execution_version=1 | ||
) | ||
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 |
Oops, something went wrong.