Skip to content

Commit f54fe4c

Browse files
charnessmattbennett
authored andcommitted
Compare Enum types (#11)
* 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.
1 parent 4bfa47b commit f54fe4c

File tree

8 files changed

+254
-10
lines changed

8 files changed

+254
-10
lines changed

sqlalchemydiff/comparer.py

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,12 @@ def compare(left_uri, right_uri, ignores=None, ignores_sep=None):
9393
tables_info.common, left_inspector, right_inspector, ignore_manager
9494
)
9595

96+
info['enums'] = _get_enums_info(
97+
left_inspector,
98+
right_inspector,
99+
ignore_manager.get('*', 'enum'),
100+
)
101+
96102
errors = _compile_errors(info)
97103
result = _make_result(info, errors)
98104

@@ -161,6 +167,7 @@ def _get_info_dict(left_uri, right_uri, tables_info):
161167
'common': tables_info.common,
162168
},
163169
'tables_data': {},
170+
'enums': {},
164171
}
165172

166173
return info
@@ -214,6 +221,13 @@ def _get_table_data(
214221
ignore_manager.get(table_name, 'col')
215222
)
216223

224+
table_data['constraints'] = _get_constraints_info(
225+
left_inspector,
226+
right_inspector,
227+
table_name,
228+
ignore_manager.get(table_name, 'cons')
229+
)
230+
217231
return table_data
218232

219233

@@ -335,6 +349,56 @@ def _get_columns(inspector, table_name):
335349
return inspector.get_columns(table_name)
336350

337351

352+
def _get_constraints_info(left_inspector, right_inspector,
353+
table_name, ignores):
354+
left_constraints_list = _get_constraints_data(left_inspector, table_name)
355+
right_constraints_list = _get_constraints_data(right_inspector, table_name)
356+
357+
left_constraints_list = _discard_ignores_by_name(left_constraints_list,
358+
ignores)
359+
right_constraints_list = _discard_ignores_by_name(right_constraints_list,
360+
ignores)
361+
362+
# process into dict
363+
left_constraints = dict((elem['name'], elem)
364+
for elem in left_constraints_list)
365+
right_constraints = dict((elem['name'], elem)
366+
for elem in right_constraints_list)
367+
368+
return _diff_dicts(left_constraints, right_constraints)
369+
370+
371+
def _get_constraints_data(inspector, table_name):
372+
try:
373+
return inspector.get_check_constraints(table_name)
374+
except (AttributeError, NotImplementedError): # pragma: no cover
375+
# sqlalchemy < 1.1.0
376+
# or a dialect that doesn't implement get_check_constraints
377+
return []
378+
379+
380+
def _get_enums_info(left_inspector, right_inspector, ignores):
381+
left_enums_list = _get_enums_data(left_inspector)
382+
right_enums_list = _get_enums_data(right_inspector)
383+
384+
left_enums_list = _discard_ignores_by_name(left_enums_list, ignores)
385+
right_enums_list = _discard_ignores_by_name(right_enums_list, ignores)
386+
387+
# process into dict
388+
left_enums = dict((elem['name'], elem) for elem in left_enums_list)
389+
right_enums = dict((elem['name'], elem) for elem in right_enums_list)
390+
391+
return _diff_dicts(left_enums, right_enums)
392+
393+
394+
def _get_enums_data(inspector):
395+
try:
396+
# as of 1.2.0, PostgreSQL dialect only; see PGInspector
397+
return inspector.get_enums()
398+
except AttributeError:
399+
return []
400+
401+
338402
def _discard_ignores_by_name(items, ignores):
339403
return [item for item in items if item['name'] not in ignores]
340404

@@ -364,6 +428,7 @@ def _compile_errors(info):
364428
errors_template = {
365429
'tables': {},
366430
'tables_data': {},
431+
'enums': {},
367432
}
368433
errors = deepcopy(errors_template)
369434

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

377442
# then check if there is a discrepancy in the data for each table
378-
keys = ['foreign_keys', 'primary_keys', 'indexes', 'columns']
443+
keys = ['foreign_keys', 'primary_keys', 'indexes', 'columns',
444+
'constraints']
379445
subkeys = ['left_only', 'right_only', 'diff']
380446

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

455+
for subkey in subkeys:
456+
if info['enums'][subkey]:
457+
errors['enums'][subkey] = info['enums'][subkey]
458+
389459
if errors != errors_template:
390460
errors['uris'] = info['uris']
391461
return errors

sqlalchemydiff/util.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ def prepare_schema_from_models(uri, sqlalchemy_base):
108108

109109
class IgnoreManager:
110110

111-
allowed_identifiers = ['pk', 'fk', 'idx', 'col']
111+
allowed_identifiers = ['pk', 'fk', 'idx', 'col', 'cons', 'enum']
112112

113113
def __init__(self, ignores, separator=None):
114114
self.separator = separator or '.'

test/endtoend/enumadaptor.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"""
2+
Adapt Enum across versions of SQLAlchemy.
3+
4+
SQLAlchemy supports PEP 435 Enum classes as of 1.1.
5+
Prior versions supported only the values as strings.
6+
7+
Export a suitable column type for either case.
8+
"""
9+
import enum
10+
import sqlalchemy
11+
12+
13+
def Enum(*enums, **kw):
14+
if sqlalchemy.__version__ >= '1.1':
15+
return sqlalchemy.Enum(*enums, **kw)
16+
17+
if len(enums) == 1 and issubclass(enums[0], enum.Enum):
18+
return sqlalchemy.Enum(*(v.name for v in enums[0]), **kw)
19+
20+
return sqlalchemy.Enum(*enums, **kw)

test/endtoend/models_left.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
# -*- coding: utf-8 -*-
2+
import enum
3+
24
from sqlalchemy import Column, ForeignKey, Integer, String, Unicode
35
from sqlalchemy.ext.declarative import declarative_base
46

7+
from .enumadaptor import Enum
8+
59

610
Base = declarative_base()
711

812

13+
class Polarity(enum.Enum):
14+
NEGATIVE = 'NEGATIVE'
15+
POSITIVE = 'POSITIVE'
16+
17+
918
class Employee(Base):
1019
__tablename__ = "employees"
1120

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

1829
company_id = Column(
1930
Integer,

test/endtoend/models_right.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
# -*- coding: utf-8 -*-
2+
import enum
3+
24
from sqlalchemy import Column, ForeignKey, Integer, String, Unicode
35
from sqlalchemy.ext.declarative import declarative_base
46

7+
from .enumadaptor import Enum
8+
59

610
Base = declarative_base()
711

812

13+
class Polarity(enum.Enum):
14+
NEG = 'NEG'
15+
POS = 'POS'
16+
17+
918
class Employee(Base):
1019
__tablename__ = "employees"
1120

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

1829
company_id = Column(
1930
Integer,

test/endtoend/test_example.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import json
33

44
import pytest
5+
from sqlalchemy import create_engine
56

67
from sqlalchemydiff.comparer import compare
78
from sqlalchemydiff.util import (
@@ -108,6 +109,39 @@ def test_errors_dict_catches_all_differences(uri_left, uri_right):
108109
}
109110
},
110111
'employees': {
112+
'columns': {
113+
'diff': [
114+
{
115+
'key': 'polarity',
116+
'left': {
117+
'default': None,
118+
'name': 'polarity',
119+
'nullable': True,
120+
'type': "ENUM('NEGATIVE','POSITIVE')"},
121+
'right': {
122+
'default': None,
123+
'name': 'polarity',
124+
'nullable': True,
125+
'type': "ENUM('NEG','POS')"
126+
}
127+
},
128+
{
129+
'key': 'spin',
130+
'left': {
131+
'default': None,
132+
'name': 'spin',
133+
'nullable': True,
134+
'type': 'VARCHAR(9)'
135+
},
136+
'right': {
137+
'default': None,
138+
'name': 'spin',
139+
'nullable': True,
140+
'type': 'VARCHAR(4)'
141+
}
142+
}
143+
]
144+
},
111145
'foreign_keys': {
112146
'left_only': [
113147
{
@@ -215,12 +249,27 @@ def test_errors_dict_catches_all_differences(uri_left, uri_right):
215249
}
216250
}
217251
},
252+
'enums': {
253+
},
218254
'uris': {
219255
'left': uri_left,
220256
'right': uri_right,
221257
}
222258
}
223259

260+
engine = create_engine(uri_left)
261+
dialect = engine.dialect
262+
if getattr(dialect, 'supports_comments', False):
263+
# sqlalchemy 1.2.0 adds support for SQL comments
264+
# expect them in the errors when supported
265+
for table in expected_errors['tables_data'].values():
266+
for column in table['columns']['diff']:
267+
for side in ['left', 'right']:
268+
column[side].update(comment=None)
269+
for side in ['left_only', 'right_only']:
270+
for column in table['columns'].get(side, []):
271+
column.update(comment=None)
272+
224273
assert not result.is_match
225274

226275
compare_error_dicts(expected_errors, result.errors)
@@ -297,8 +346,11 @@ def test_ignores(uri_left, uri_right):
297346
ignores = [
298347
'mobile_numbers',
299348
'phone_numbers',
349+
'*.enum.polarity',
300350
'companies.col.name',
301351
'companies.idx.name',
352+
'employees.col.polarity',
353+
'employees.col.spin',
302354
'employees.fk.fk_employees_companies',
303355
'employees.fk.fk_emp_comp',
304356
'employees.idx.ix_employees_name',
@@ -328,8 +380,11 @@ def test_ignores_alternative_sep(uri_left, uri_right):
328380
ignores = [
329381
'mobile_numbers',
330382
'phone_numbers',
383+
'*#enum#polarity',
331384
'companies#col#name',
332385
'companies#idx#name',
386+
'employees#col#polarity',
387+
'employees#col#spin',
333388
'employees#fk#fk_employees_companies',
334389
'employees#fk#fk_emp_comp',
335390
'employees#idx#ix_employees_name',
@@ -353,6 +408,7 @@ def test_ignores_alternative_sep(uri_left, uri_right):
353408
@pytest.mark.parametrize('missing_ignore', [
354409
'mobile_numbers',
355410
'phone_numbers',
411+
'*.enum.polarity',
356412
'companies.col.name',
357413
'companies.idx.name',
358414
'employees.fk.fk_employees_companies',
@@ -375,6 +431,7 @@ def test_ignores_all_needed(uri_left, uri_right, missing_ignore):
375431
ignores = [
376432
'mobile_numbers',
377433
'phone_numbers',
434+
'*.enum.polarity',
378435
'companies.col.name',
379436
'companies.idx.name',
380437
'employees.fk.fk_employees_companies',

0 commit comments

Comments
 (0)