Skip to content

Commit

Permalink
Merge pull request #203 from PolyJIT/b/fix-migration-if-not-owner
Browse files Browse the repository at this point in the history
Provide sane error handling for SQLAlchemy errors

Former-commit-id: 39616d481f2f7a99ac0b297829de5967e7ccc756
Former-commit-id: 588d6cf42fca4987049cc2dcf702386ed682fa33 [formerly b3b116d482b4e0b1bd7d7cc1a547ec1378f5c02d] [formerly a68bc82f22ba4b13a139e5297f1ad08005bdef90 [formerly cc46ccdfdc8bc49cb50d8a2a33994afcf23d7256]]
Former-commit-id: 48b212d9203f835ee83ba407cc6467ecc2ea4522 [formerly 3ac13aa31022ce941721037fe24c6ebede1e7698]
Former-commit-id: 23fced2c96381839cb1d3acc02e4d3b835c56176
  • Loading branch information
simbuerg authored Jul 12, 2018
2 parents 999ae42 + f4d5924 commit 59cf610
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 16 deletions.
29 changes: 23 additions & 6 deletions benchbuild/db/versions/001_Remove_RegressionTest_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
This table can and should be reintroduced by an experiment that requires it.
"""
from sqlalchemy import Table, Column, ForeignKey, Integer, String
from benchbuild.utils.schema import metadata
import sqlalchemy as sa
from sqlalchemy import Column, ForeignKey, Integer, String, Table

from benchbuild.utils.schema import exceptions, metadata

META = metadata()
REGRESSION = Table('regressions', META,
Expand All @@ -19,10 +21,25 @@


def upgrade(migrate_engine):
META.bind = migrate_engine
REGRESSION.drop()
@exceptions(
error_is_fatal=False,
error_messages={
sa.exc.ProgrammingError:
"Removing table 'Regressions' failed. Please delete the table manually"
})
def do_upgrade():
META.bind = migrate_engine
REGRESSION.drop()

do_upgrade()


def downgrade(migrate_engine):
META.bind = migrate_engine
REGRESSION.create()
@exceptions(error_messages={
sa.exc.ProgrammingError: "Adding table 'Regressions' failed."
})
def do_downgrade():
META.bind = migrate_engine
REGRESSION.create()

do_downgrade()
30 changes: 24 additions & 6 deletions benchbuild/db/versions/002_Remove_GlobalConfig_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
This table can and should be reintroduced by an experiment that requires it.
"""
from sqlalchemy import Table, Column, ForeignKey, Integer, String
from benchbuild.utils.schema import metadata, GUID

import sqlalchemy as sa
from sqlalchemy import Column, ForeignKey, String, Table

from benchbuild.utils.schema import GUID, exceptions, metadata

META = metadata()
GLOBAL = Table('globalconfig', META,
Expand All @@ -19,10 +22,25 @@


def upgrade(migrate_engine):
META.bind = migrate_engine
GLOBAL.drop()
@exceptions(
error_is_fatal=False,
error_messages={
sa.exc.ProgrammingError:
"Removing table 'globalconfig' failed. Please delete the table manually"
})
def do_upgrade():
META.bind = migrate_engine
GLOBAL.drop()

do_upgrade()


def downgrade(migrate_engine):
META.bind = migrate_engine
GLOBAL.create()
@exceptions(error_messages={
sa.exc.ProgrammingError: "Adding table 'globalconfig' failed."
})
def do_downgrade():
META.bind = migrate_engine
GLOBAL.create()

do_downgrade()
12 changes: 9 additions & 3 deletions benchbuild/db/versions/003_Unmanage_Events.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"""
from sqlalchemy import (MetaData, SmallInteger, BigInteger, Numeric, Table,
Column, ForeignKey, Integer, String)
from benchbuild.utils.schema import Run, RunGroup, Project, Experiment
import sqlalchemy as sa
from benchbuild.utils.schema import exceptions

META = MetaData()

Expand All @@ -33,5 +34,10 @@ def upgrade(migrate_engine):


def downgrade(migrate_engine):
META.bind = migrate_engine
EVENTS.create(checkfirst=True)
@exceptions(error_messages={
sa.exc.ProgrammingError:
"Adding table 'benchbuild_events' failed."
})
def do_downgrade():
META.bind = migrate_engine
EVENTS.create()
61 changes: 60 additions & 1 deletion benchbuild/utils/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
paths for you.
"""

import functools
import logging
import sys
import uuid
Expand All @@ -32,7 +33,7 @@
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from sqlalchemy.types import (CHAR, Float, TypeDecorator)
from sqlalchemy.types import CHAR, Float, TypeDecorator

import benchbuild.settings as settings
import benchbuild.utils.user_interface as ui
Expand All @@ -46,6 +47,48 @@ def metadata():
return BASE.metadata


def exceptions(error_is_fatal=True, error_messages=None):
"""
Handle SQLAlchemy exceptions in a sane way.
Args:
func: An arbitrary function to wrap.
error_is_fatal: Should we exit the program on exception?
reraise: Should we reraise the exception, after logging? Only makes sense
if error_is_fatal is False.
error_messages: A dictionary that assigns an exception class to a
customized error message.
"""

def exception_decorator(func):
nonlocal error_messages

@functools.wraps(func)
def exc_wrapper(*args, **kwargs):
nonlocal error_messages
try:
result = func(*args, **kwargs)
except sa.exc.SQLAlchemyError as err:
result = None
details = None
err_type = err.__class__
if error_messages and err_type in error_messages:
details = error_messages[err_type]
if details:
LOG.error(details)
LOG.error("For developers: (%s) %s", err.__class__, str(err))
if error_is_fatal:
sys.exit("Abort, SQL operation failed.")
if not ui.ask(
"I can continue at your own risk, do you want that?"):
raise err
return result

return exc_wrapper

return exception_decorator


class GUID(TypeDecorator):
"""Platform-independent GUID type.
Expand Down Expand Up @@ -306,6 +349,11 @@ def get_version_data():
return (connect_str, repo_url)


@exceptions(
error_messages={
sa.exc.ProgrammingError:
"Could not enforce versioning. Are you allowed to modify the database?"
})
def enforce_versioning(force=False):
"""Install versioning on the db."""
connect_str, repo_url = get_version_data()
Expand Down Expand Up @@ -335,6 +383,12 @@ def setup_versioning():
return (repo_version, db_version)


@exceptions(
error_messages={
sa.exc.ProgrammingError:
"Update failed."
" Base schema version diverged from the expected structure."
})
def maybe_update_db(repo_version, db_version):
if db_version is None:
return
Expand All @@ -349,6 +403,7 @@ def maybe_update_db(repo_version, db_version):
if not ui.ask("Should I attempt to update your schema to version '{0}'?".
format(repo_version)):
LOG.error("User declined schema upgrade.")
return

connect_str = settings.CFG["db"]["connect_string"].value()
repo_url = bbpath.template_path("../db/")
Expand Down Expand Up @@ -390,6 +445,10 @@ def configure_engine(self):
LOG.warning("Unable to set isolation level to READ COMMITTED")
return True

@exceptions(error_messages={
sa.exc.NoSuchModuleError:
"Connect string contained an invalid backend."
})
def __init__(self):
self.__test_mode = settings.CFG['db']['rollback'].value()
self.engine = create_engine(
Expand Down

0 comments on commit 59cf610

Please sign in to comment.