Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🧪 TESTS: SQLA Migrations -> pytest #5192

Merged
merged 42 commits into from
Dec 8, 2021
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
6d759a5
🧪 TESTS: SQLA Migrations -> pytest
chrisjsewell Oct 22, 2021
5a7467d
Merge branch 'develop' into update-migrate-tests
chrisjsewell Oct 22, 2021
659673f
create_user -> init_db
chrisjsewell Oct 22, 2021
e44f0f8
Merge remote-tracking branch 'upstream/develop' into update-migrate-t…
chrisjsewell Oct 28, 2021
83a4e6f
Change names
chrisjsewell Oct 28, 2021
5c42be7
Merge branch 'develop' into update-migrate-tests
chrisjsewell Oct 28, 2021
d382718
Merge branch 'develop' into update-migrate-tests
chrisjsewell Nov 1, 2021
e3edbe4
Update tests/backends/aiida_sqlalchemy/migrations/test_all_basic.py
chrisjsewell Nov 1, 2021
6483b08
more splitting of files
chrisjsewell Nov 2, 2021
962d4fb
Merge branch 'develop' into update-migrate-tests
chrisjsewell Nov 2, 2021
aee33ae
Update module docstrings
chrisjsewell Nov 2, 2021
61e7562
rename
chrisjsewell Nov 2, 2021
fc9d4da
test commenting out tests/backends/aiida_sqlalchemy/test_session.py
chrisjsewell Nov 2, 2021
bd326c4
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 2, 2021
ce48ab4
fix stall
chrisjsewell Nov 2, 2021
f1efe48
Merge branch 'develop' into update-migrate-tests
chrisjsewell Nov 2, 2021
acea111
fix deprecation
chrisjsewell Nov 3, 2021
24a871e
add fixes
chrisjsewell Nov 3, 2021
2bd39eb
check
chrisjsewell Nov 3, 2021
59d6d03
Create test_session.py
chrisjsewell Nov 3, 2021
f2bc475
check
chrisjsewell Nov 3, 2021
f725fbe
update
chrisjsewell Nov 8, 2021
2b988a8
Merge branch 'develop' into update-migrate-tests
chrisjsewell Nov 8, 2021
f1301aa
Update conftest.py
chrisjsewell Nov 8, 2021
cbc337a
Update conftest.py
chrisjsewell Nov 8, 2021
cf4b10d
check
chrisjsewell Nov 8, 2021
a327b79
Update test_10_group_update.py
chrisjsewell Nov 8, 2021
c463220
check
chrisjsewell Nov 9, 2021
7733d45
Update conftest.py
chrisjsewell Nov 11, 2021
cd534be
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 11, 2021
8368bfb
skip hanging
chrisjsewell Nov 11, 2021
2130e1f
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 11, 2021
dc30add
Update test_11_v2_repository.py
chrisjsewell Nov 11, 2021
a563d47
revert
chrisjsewell Nov 11, 2021
1434fb9
Merge branch 'develop' into update-migrate-tests
chrisjsewell Nov 12, 2021
13386fd
Merge branch 'develop' into update-migrate-tests
chrisjsewell Dec 1, 2021
d1653da
Merge branch 'develop' into update-migrate-tests
chrisjsewell Dec 8, 2021
aa40537
try comment out test_session
chrisjsewell Dec 8, 2021
2f8ea23
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Dec 8, 2021
7aa86a5
uncomment
chrisjsewell Dec 8, 2021
705ff0d
fix pre-commit
chrisjsewell Dec 8, 2021
cfdd873
Update test_session.py
chrisjsewell Dec 8, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 3 additions & 8 deletions aiida/backends/djsite/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,9 @@ def is_database_schema_ahead(self):
"""
# For Django the versions numbers are numerical so we can compare them
from distutils.version import StrictVersion
return StrictVersion(self.get_schema_version_database()) > StrictVersion(self.get_schema_version_code())
return StrictVersion(self.get_schema_version_backend()) > StrictVersion(self.get_schema_version_head())

def get_schema_version_code(self):
"""Return the code schema version."""
def get_schema_version_head(self):
from .db.models import SCHEMA_VERSION
return SCHEMA_VERSION

Expand Down Expand Up @@ -101,11 +100,7 @@ def get_schema_generation_database(self):
except (IndexError, ValueError, TypeError):
return '1'

def get_schema_version_database(self):
"""Return the database schema version.

:return: `distutils.version.StrictVersion` with schema version of the database
"""
def get_schema_version_backend(self):
from django.db.utils import ProgrammingError

from aiida.manage.manager import get_manager
Expand Down
25 changes: 11 additions & 14 deletions aiida/backends/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ def migrate(self):
self.validate_schema_generation_for_migration()
self._migrate_database_generation()

if self.get_schema_version_code() != self.get_schema_version_database():
if self.get_schema_version_head() != self.get_schema_version_backend():
self.validate_schema_version_for_migration()
self._migrate_database_version()

Expand All @@ -160,7 +160,7 @@ def _migrate_database_generation(self):
logic that is required.
"""
self.set_schema_generation_database(SCHEMA_GENERATION_VALUE)
self.set_schema_version_database(self.get_schema_version_code())
self.set_schema_version_database(self.get_schema_version_head())

def _migrate_database_version(self):
"""Migrate the database to the current schema version.
Expand All @@ -179,8 +179,8 @@ def is_database_schema_ahead(self):
"""

@abc.abstractmethod
def get_schema_version_code(self):
"""Return the code schema version."""
def get_schema_version_head(self):
"""Return the latest schema version."""
sphuber marked this conversation as resolved.
Show resolved Hide resolved

@abc.abstractmethod
def get_schema_version_reset(self, schema_generation_code):
Expand All @@ -191,11 +191,8 @@ def get_schema_version_reset(self, schema_generation_code):
"""

@abc.abstractmethod
def get_schema_version_database(self):
"""Return the database schema version.

:return: `distutils.version.LooseVersion` with schema version of the database
"""
def get_schema_version_backend(self):
"""Return the current schema version."""
sphuber marked this conversation as resolved.
Show resolved Hide resolved

@abc.abstractmethod
def set_schema_version_database(self, version):
Expand Down Expand Up @@ -256,7 +253,7 @@ def validate_schema_generation_for_migration(self):
"""
schema_generation_code = SCHEMA_GENERATION_VALUE
schema_generation_database = self.get_schema_generation_database()
schema_version_database = self.get_schema_version_database()
schema_version_database = self.get_schema_version_backend()
schema_version_reset = self.get_schema_version_reset(schema_generation_code)
schema_generation_reset, aiida_core_version_reset = SCHEMA_GENERATION_RESET[schema_generation_code]

Expand Down Expand Up @@ -287,8 +284,8 @@ def validate_schema_version_for_migration(self):

:raises `aiida.common.exceptions.IncompatibleDatabaseSchema`: if database schema version cannot be migrated
"""
schema_version_code = self.get_schema_version_code()
schema_version_database = self.get_schema_version_database()
schema_version_code = self.get_schema_version_head()
schema_version_database = self.get_schema_version_backend()

if self.is_database_schema_ahead():
# Database is newer than the code so a downgrade would be necessary but this is not supported.
Expand Down Expand Up @@ -322,8 +319,8 @@ def validate_schema_version(self, profile):
:param profile: the profile for which to validate the database schema
:raises `aiida.common.exceptions.IncompatibleDatabaseSchema`: if database schema version is not up-to-date
"""
schema_version_code = self.get_schema_version_code()
schema_version_database = self.get_schema_version_database()
schema_version_code = self.get_schema_version_head()
schema_version_database = self.get_schema_version_backend()

if schema_version_database != schema_version_code:
raise exceptions.IncompatibleDatabaseSchema(
Expand Down
43 changes: 30 additions & 13 deletions aiida/backends/sqlalchemy/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import contextlib
import os

from alembic.command import downgrade, upgrade
import sqlalchemy
from sqlalchemy.orm.exc import NoResultFound

Expand Down Expand Up @@ -94,6 +95,14 @@ def reset_backend_environment(self):
from . import reset_session
reset_session()

def list_schema_versions(self):
"""List all available schema versions (oldest to latest).

:return: list of strings with schema versions
"""
with self.alembic_script() as script:
return list(reversed([entry.revision for entry in script.walk_revisions()]))

def is_database_schema_ahead(self):
"""Determine whether the database schema version is ahead of the code schema version.

Expand All @@ -102,10 +111,9 @@ def is_database_schema_ahead(self):
:return: boolean, True if the database schema version is ahead of the code schema version.
"""
with self.alembic_script() as script:
return self.get_schema_version_database() not in [entry.revision for entry in script.walk_revisions()]
return self.get_schema_version_backend() not in [entry.revision for entry in script.walk_revisions()]

def get_schema_version_code(self):
"""Return the code schema version."""
def get_schema_version_head(self):
with self.alembic_script() as script:
return script.get_current_head()

Expand All @@ -117,11 +125,7 @@ def get_schema_version_reset(self, schema_generation_code):
"""
return SCHEMA_VERSION_RESET[schema_generation_code]

def get_schema_version_database(self):
"""Return the database schema version.

:return: `distutils.version.StrictVersion` with schema version of the database
"""
def get_schema_version_backend(self):
with self.migration_context() as context:
return context.get_current_revision()

Expand All @@ -133,13 +137,26 @@ def set_schema_version_database(self, version):
with self.migration_context() as context:
return context.stamp(context.script, 'head')

def _migrate_database_version(self):
"""Migrate the database to the current schema version."""
super()._migrate_database_version()
from alembic.command import upgrade
def migrate_up(self, version: str):
"""Migrate the database up to a specific version.

:param version: string with schema version to migrate to
"""
with self.alembic_config() as config:
upgrade(config, version)

def migrate_down(self, version: str):
"""Migrate the database down to a specific version.

:param version: string with schema version to migrate to
"""
with self.alembic_config() as config:
upgrade(config, 'head')
downgrade(config, version)

def _migrate_database_version(self):
"""Migrate the database to the latest schema version."""
super()._migrate_database_version()
self.migrate_up('head')


class SqlaSettingsManager(SettingsManager):
Expand Down
2 changes: 1 addition & 1 deletion aiida/cmdline/commands/cmd_database.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def database_version():
echo.echo('Generation: ', bold=True, nl=False)
echo.echo(backend_manager.get_schema_generation_database())
echo.echo('Revision: ', bold=True, nl=False)
echo.echo(backend_manager.get_schema_version_database())
echo.echo(backend_manager.get_schema_version_backend())


@verdi_database.command('migrate')
Expand Down
9 changes: 5 additions & 4 deletions aiida/manage/tests/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,8 @@ def use_profile(self, profile_name):
def has_profile_open(self):
return self._manager and self._manager.has_profile_open()

def reset_db(self):
return self._manager.reset_db()
def reset_db(self, init_db=True):
return self._manager.reset_db(init_db=init_db)

def destroy_all(self):
if self._manager:
Expand Down Expand Up @@ -166,10 +166,11 @@ def _select_db_test_case(self, backend):
self._test_case = SqlAlchemyTests()
self._test_case.test_session = get_scoped_session()

def reset_db(self):
def reset_db(self, init_db=True):
self._test_case.clean_db() # will drop all users
manager.reset_manager()
self.init_db()
if init_db:
self.init_db()

def init_db(self):
"""Initialise the database state for running of tests.
Expand Down
Empty file.
90 changes: 90 additions & 0 deletions tests/backends/aiida_sqlalchemy/migrations/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# -*- coding: utf-8 -*-
###########################################################################
# Copyright (c), The AiiDA team. All rights reserved. #
# This file is part of the AiiDA code. #
# #
# The code is hosted on GitHub at https://github.com/aiidateam/aiida-core #
# For further information on the license, see the LICENSE.txt file #
# For further information please visit http://www.aiida.net #
###########################################################################
"""Tests for the migration engine (Alembic) as well as for the AiiDA migrations for SQLAlchemy."""
from contextlib import contextmanager
from typing import Iterator

import pytest
from sqlalchemy.orm import Session

from aiida.backends.sqlalchemy.manager import SqlaBackendManager


class Migrator:
"""A class to yield from the ``perform_migrations`` fixture."""

def __init__(self, backend, manager: SqlaBackendManager) -> None:
self.backend = backend
self._manager = manager

def migrate_up(self, revision: str) -> None:
"""Migrate up to a given revision."""
self._manager.migrate_up(revision)
assert self._manager.get_schema_version_backend() == revision

def migrate_down(self, revision: str) -> None:
"""Migrate down to a given revision."""
self._manager.migrate_down(revision)
assert self._manager.get_schema_version_backend() == revision

def get_current_table(self, table_name):
"""
Return a Model instantiated at the correct migration.
Note that this is obtained by inspecting the database and not
by looking into the models file.
So, special methods possibly defined in the models files/classes are not present.

For instance, you can do::

DbGroup = self.get_current_table('db_dbgroup')

:param table_name: the name of the table.
"""
from alembic.migration import MigrationContext # pylint: disable=import-error
from sqlalchemy.ext.automap import automap_base # pylint: disable=import-error,no-name-in-module

with self.backend.get_session().bind.begin() as connection:
context = MigrationContext.configure(connection)
bind = context.bind

base = automap_base()
# reflect the tables
base.prepare(bind.engine, reflect=True)

return getattr(base.classes, table_name)

@contextmanager
def session(self) -> Iterator[Session]:
"""A context manager for a new session."""
with self.backend.get_session().bind.begin() as connection:
session = Session(connection.engine, future=True)
try:
yield session
except Exception:
session.rollback()
raise
finally:
session.close()


@pytest.fixture()
def perform_migrations(aiida_profile, backend, request):
"""A fixture to setup the database for migration tests"""
# note downgrading to 1830c8430131 requires adding columns to `DbUser` and hangs if a user is present
aiida_profile.reset_db(init_db=False)
migrator = Migrator(backend, SqlaBackendManager())
marker = request.node.get_closest_marker('migrate_down')
if marker is not None:
assert marker.args, 'No version given'
migrator.migrate_down(marker.args[0])
yield migrator
# ensure that the database is migrated back up to the latest version, once finished
aiida_profile.reset_db(init_db=False)
SqlaBackendManager().migrate_up('head')
35 changes: 35 additions & 0 deletions tests/backends/aiida_sqlalchemy/migrations/test_all_basic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
###########################################################################
# Copyright (c), The AiiDA team. All rights reserved. #
# This file is part of the AiiDA code. #
# #
# The code is hosted on GitHub at https://github.com/aiidateam/aiida-core #
# For further information on the license, see the LICENSE.txt file #
# For further information please visit http://www.aiida.net #
###########################################################################
"""Basic tests for all migratios"""
import pytest


@pytest.mark.usefixtures('perform_migrations')
def test_all_empty_migrations():
"""Test migrating down to a particular version, then back up, using an empty database.

Note, migrating down+up with 59edaf8a8b79_adding_indexes_and_constraints_to_the_.py raises::

sqlalchemy.exc.ProgrammingError:
(psycopg2.errors.DuplicateTable) relation "db_dbgroup_dbnodes_dbgroup_id_dbnode_id_key" already exists

So we only run for all versions later than this.
"""
from aiida.backends.sqlalchemy.manager import SqlaBackendManager
migrator = SqlaBackendManager()
all_versions = migrator.list_schema_versions()
first_index = all_versions.index('a514d673c163') + 1
# ideally we would pytest parametrize this, but then we would need to call list_schema_versions on module load
for version in all_versions[first_index:]:
print(version)
chrisjsewell marked this conversation as resolved.
Show resolved Hide resolved
migrator.migrate_down(version)
assert migrator.get_schema_version_backend() == version
migrator.migrate_up('head')
assert migrator.get_schema_version_backend() == migrator.get_schema_version_head()
Loading