Skip to content

Commit

Permalink
Compare Enum types (#11)
Browse files Browse the repository at this point in the history
* Add comparison of SQLAlchemy Enums

Compare enum types and/or check constraints where supported by the
database.

* Add tests for enum comparisons

Besides the test changes themselves,

- Update mysql-connector-python to 2.1.6 that's currently available
  from Oracle.
- Suppress "not-callable" errors from pylint.
- Fix flake8 indentation errors.

* Add ignores for the differing enum types

* Address version and dialect differences

Explain the differences in comments and tolerate via try...except.

* Expect comments in errors when supported

SQLAlchemy 1.2.0 added support for SQL comments. Since the test
example models include no comments, these come back as None in the
inspection output.

When the dialect indicates it `supports_comments` (a new attribute in
1.2.0), add `"comment": None` to each column in the expected_errors.

* Adapt tests' native enums for SQLAlchemy 1.0

SQLAlchemy supports PEP 435 Enum classes as of 1.1.

In order to exercise get_check_constraints-related code aimed
at < 1.1.0, adapt the Polarity (native enum) columns to the
1.0 Enum API when using 1.0.

* Exempt get_check_constraints protection from coverage

Testing with sqlalchemy 1.2 and mysql raises neither the
AttributeError nor the NotImplementedError in _get_constraints_data.
Disable coverage checking for the except clause so tests can pass.
  • Loading branch information
charness authored and mattbennett committed Jan 25, 2018
1 parent 4bfa47b commit f54fe4c
Show file tree
Hide file tree
Showing 8 changed files with 254 additions and 10 deletions.
72 changes: 71 additions & 1 deletion sqlalchemydiff/comparer.py
Expand Up @@ -93,6 +93,12 @@ def compare(left_uri, right_uri, ignores=None, ignores_sep=None):
tables_info.common, left_inspector, right_inspector, ignore_manager
)

info['enums'] = _get_enums_info(
left_inspector,
right_inspector,
ignore_manager.get('*', 'enum'),
)

errors = _compile_errors(info)
result = _make_result(info, errors)

Expand Down Expand Up @@ -161,6 +167,7 @@ def _get_info_dict(left_uri, right_uri, tables_info):
'common': tables_info.common,
},
'tables_data': {},
'enums': {},
}

return info
Expand Down Expand Up @@ -214,6 +221,13 @@ def _get_table_data(
ignore_manager.get(table_name, 'col')
)

table_data['constraints'] = _get_constraints_info(
left_inspector,
right_inspector,
table_name,
ignore_manager.get(table_name, 'cons')
)

return table_data


Expand Down Expand Up @@ -335,6 +349,56 @@ def _get_columns(inspector, table_name):
return inspector.get_columns(table_name)


def _get_constraints_info(left_inspector, right_inspector,
table_name, ignores):
left_constraints_list = _get_constraints_data(left_inspector, table_name)
right_constraints_list = _get_constraints_data(right_inspector, table_name)

left_constraints_list = _discard_ignores_by_name(left_constraints_list,
ignores)
right_constraints_list = _discard_ignores_by_name(right_constraints_list,
ignores)

# process into dict
left_constraints = dict((elem['name'], elem)
for elem in left_constraints_list)
right_constraints = dict((elem['name'], elem)
for elem in right_constraints_list)

return _diff_dicts(left_constraints, right_constraints)


def _get_constraints_data(inspector, table_name):
try:
return inspector.get_check_constraints(table_name)
except (AttributeError, NotImplementedError): # pragma: no cover
# sqlalchemy < 1.1.0
# or a dialect that doesn't implement get_check_constraints
return []


def _get_enums_info(left_inspector, right_inspector, ignores):
left_enums_list = _get_enums_data(left_inspector)
right_enums_list = _get_enums_data(right_inspector)

left_enums_list = _discard_ignores_by_name(left_enums_list, ignores)
right_enums_list = _discard_ignores_by_name(right_enums_list, ignores)

# process into dict
left_enums = dict((elem['name'], elem) for elem in left_enums_list)
right_enums = dict((elem['name'], elem) for elem in right_enums_list)

return _diff_dicts(left_enums, right_enums)


def _get_enums_data(inspector):
try:
# as of 1.2.0, PostgreSQL dialect only; see PGInspector
return inspector.get_enums()
except AttributeError:
return []


def _discard_ignores_by_name(items, ignores):
return [item for item in items if item['name'] not in ignores]

Expand Down Expand Up @@ -364,6 +428,7 @@ def _compile_errors(info):
errors_template = {
'tables': {},
'tables_data': {},
'enums': {},
}
errors = deepcopy(errors_template)

Expand All @@ -375,7 +440,8 @@ def _compile_errors(info):
errors['tables']['right_only'] = info['tables']['right_only']

# then check if there is a discrepancy in the data for each table
keys = ['foreign_keys', 'primary_keys', 'indexes', 'columns']
keys = ['foreign_keys', 'primary_keys', 'indexes', 'columns',
'constraints']
subkeys = ['left_only', 'right_only', 'diff']

for table_name in info['tables_data']:
Expand All @@ -386,6 +452,10 @@ def _compile_errors(info):
table_d.setdefault(key, {})[subkey] = info[
'tables_data'][table_name][key][subkey]

for subkey in subkeys:
if info['enums'][subkey]:
errors['enums'][subkey] = info['enums'][subkey]

if errors != errors_template:
errors['uris'] = info['uris']
return errors
Expand Down
2 changes: 1 addition & 1 deletion sqlalchemydiff/util.py
Expand Up @@ -108,7 +108,7 @@ def prepare_schema_from_models(uri, sqlalchemy_base):

class IgnoreManager:

allowed_identifiers = ['pk', 'fk', 'idx', 'col']
allowed_identifiers = ['pk', 'fk', 'idx', 'col', 'cons', 'enum']

def __init__(self, ignores, separator=None):
self.separator = separator or '.'
Expand Down
20 changes: 20 additions & 0 deletions test/endtoend/enumadaptor.py
@@ -0,0 +1,20 @@
"""
Adapt Enum across versions of SQLAlchemy.
SQLAlchemy supports PEP 435 Enum classes as of 1.1.
Prior versions supported only the values as strings.
Export a suitable column type for either case.
"""
import enum
import sqlalchemy


def Enum(*enums, **kw):
if sqlalchemy.__version__ >= '1.1':
return sqlalchemy.Enum(*enums, **kw)

if len(enums) == 1 and issubclass(enums[0], enum.Enum):
return sqlalchemy.Enum(*(v.name for v in enums[0]), **kw)

return sqlalchemy.Enum(*enums, **kw)
11 changes: 11 additions & 0 deletions test/endtoend/models_left.py
@@ -1,11 +1,20 @@
# -*- coding: utf-8 -*-
import enum

from sqlalchemy import Column, ForeignKey, Integer, String, Unicode
from sqlalchemy.ext.declarative import declarative_base

from .enumadaptor import Enum


Base = declarative_base()


class Polarity(enum.Enum):
NEGATIVE = 'NEGATIVE'
POSITIVE = 'POSITIVE'


class Employee(Base):
__tablename__ = "employees"

Expand All @@ -14,6 +23,8 @@ class Employee(Base):
age = Column(Integer, nullable=False, default=21)
ssn = Column(Unicode(30), nullable=False)
number_of_pets = Column(Integer, default=1, nullable=False)
polarity = Column(Enum(Polarity, native_enum=True))
spin = Column(Enum('spin_down', 'spin_up', native_enum=False))

company_id = Column(
Integer,
Expand Down
11 changes: 11 additions & 0 deletions test/endtoend/models_right.py
@@ -1,11 +1,20 @@
# -*- coding: utf-8 -*-
import enum

from sqlalchemy import Column, ForeignKey, Integer, String, Unicode
from sqlalchemy.ext.declarative import declarative_base

from .enumadaptor import Enum


Base = declarative_base()


class Polarity(enum.Enum):
NEG = 'NEG'
POS = 'POS'


class Employee(Base):
__tablename__ = "employees"

Expand All @@ -14,6 +23,8 @@ class Employee(Base):
age = Column(Integer, nullable=False, default=21)
ssn = Column(Unicode(30), nullable=False)
number_of_pets = Column(Integer, default=1, nullable=False)
polarity = Column(Enum(Polarity, native_enum=True))
spin = Column(Enum('down', 'up', native_enum=False))

company_id = Column(
Integer,
Expand Down
57 changes: 57 additions & 0 deletions test/endtoend/test_example.py
Expand Up @@ -2,6 +2,7 @@
import json

import pytest
from sqlalchemy import create_engine

from sqlalchemydiff.comparer import compare
from sqlalchemydiff.util import (
Expand Down Expand Up @@ -108,6 +109,39 @@ def test_errors_dict_catches_all_differences(uri_left, uri_right):
}
},
'employees': {
'columns': {
'diff': [
{
'key': 'polarity',
'left': {
'default': None,
'name': 'polarity',
'nullable': True,
'type': "ENUM('NEGATIVE','POSITIVE')"},
'right': {
'default': None,
'name': 'polarity',
'nullable': True,
'type': "ENUM('NEG','POS')"
}
},
{
'key': 'spin',
'left': {
'default': None,
'name': 'spin',
'nullable': True,
'type': 'VARCHAR(9)'
},
'right': {
'default': None,
'name': 'spin',
'nullable': True,
'type': 'VARCHAR(4)'
}
}
]
},
'foreign_keys': {
'left_only': [
{
Expand Down Expand Up @@ -215,12 +249,27 @@ def test_errors_dict_catches_all_differences(uri_left, uri_right):
}
}
},
'enums': {
},
'uris': {
'left': uri_left,
'right': uri_right,
}
}

engine = create_engine(uri_left)
dialect = engine.dialect
if getattr(dialect, 'supports_comments', False):
# sqlalchemy 1.2.0 adds support for SQL comments
# expect them in the errors when supported
for table in expected_errors['tables_data'].values():
for column in table['columns']['diff']:
for side in ['left', 'right']:
column[side].update(comment=None)
for side in ['left_only', 'right_only']:
for column in table['columns'].get(side, []):
column.update(comment=None)

assert not result.is_match

compare_error_dicts(expected_errors, result.errors)
Expand Down Expand Up @@ -297,8 +346,11 @@ def test_ignores(uri_left, uri_right):
ignores = [
'mobile_numbers',
'phone_numbers',
'*.enum.polarity',
'companies.col.name',
'companies.idx.name',
'employees.col.polarity',
'employees.col.spin',
'employees.fk.fk_employees_companies',
'employees.fk.fk_emp_comp',
'employees.idx.ix_employees_name',
Expand Down Expand Up @@ -328,8 +380,11 @@ def test_ignores_alternative_sep(uri_left, uri_right):
ignores = [
'mobile_numbers',
'phone_numbers',
'*#enum#polarity',
'companies#col#name',
'companies#idx#name',
'employees#col#polarity',
'employees#col#spin',
'employees#fk#fk_employees_companies',
'employees#fk#fk_emp_comp',
'employees#idx#ix_employees_name',
Expand All @@ -353,6 +408,7 @@ def test_ignores_alternative_sep(uri_left, uri_right):
@pytest.mark.parametrize('missing_ignore', [
'mobile_numbers',
'phone_numbers',
'*.enum.polarity',
'companies.col.name',
'companies.idx.name',
'employees.fk.fk_employees_companies',
Expand All @@ -375,6 +431,7 @@ def test_ignores_all_needed(uri_left, uri_right, missing_ignore):
ignores = [
'mobile_numbers',
'phone_numbers',
'*.enum.polarity',
'companies.col.name',
'companies.idx.name',
'employees.fk.fk_employees_companies',
Expand Down

0 comments on commit f54fe4c

Please sign in to comment.