diff --git a/config/config.yaml b/config/config.yaml new file mode 100644 index 0000000..53c7a35 --- /dev/null +++ b/config/config.yaml @@ -0,0 +1,2 @@ +DB_URIS: + "test": "mysql+mysqlconnector://root:password@dockermachine:3306/sqlalchemydiff" \ No newline at end of file diff --git a/dists/mysql-connector-python-2.0.4.zip b/dists/mysql-connector-python-2.0.4.zip deleted file mode 100644 index 3ff4824..0000000 Binary files a/dists/mysql-connector-python-2.0.4.zip and /dev/null differ diff --git a/docs/source/conf.py b/docs/source/conf.py index fb87b1f..792cdcd 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -61,9 +61,9 @@ # built documents. # # The short X.Y version. -version = '0.0.1' +version = '0.1.0' # The full version, including alpha/beta/rc tags. -release = '0.0.1' +release = '0.1.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/source/index.rst b/docs/source/index.rst index 6c281d1..fcb0197 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -77,6 +77,20 @@ them by looking at the ``errors`` dict on the ``result``: } +You can also tell the comparer to ignore parts of the database. + +For example, to ignore the ``users`` table completely, and the ``line3`` column +of the table ``addresses`` you would call ``compare`` like this: + +.. code-block:: Python + + >>> result = compare( + uri_left, + uri_right, + ignores=['users', 'addresses.col.line3'] + ) + + If you wish to persist that dict to a JSON file, you can quickly do so by calling ``result.dump_errors()``. @@ -91,6 +105,8 @@ Currently the library can detect the following differences: - Differences in **Foreign Keys** for a common table - Differences in **Indexes** for a common table - Differences in **Columns** for a common table +- Ability to ignore a **whole table** +- Ability to ignore **primary/foreign keys**, **indexes** and **columns** Installation diff --git a/setup.py b/setup.py index cd2d8b8..91a2003 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ setup( name='sqlalchemy-diff', - version='0.0.4', + version='0.1.0', description='Compare two database schemas using sqlalchemy.', long_description=readme, author='student.com', @@ -22,21 +22,19 @@ packages=find_packages(exclude=['docs', 'test', 'test.*']), install_requires=[ "six==1.10.0", - "mock>=1.3.0", - "sqlalchemy-utils==0.31.2", + "mock>=2.0.0", + "sqlalchemy-utils==0.32.4", + "pyyaml==3.11", ], extras_require={ 'dev': [ "mysql-connector-python==2.0.4", - "pytest==2.8.2", + "pytest==2.9.1", ], 'docs': [ - "Sphinx==1.3.1", + "sphinx==1.4.1", ], }, - dependency_links=[ - "dists/mysql-connector-python-2.0.4.zip", - ], zip_safe=True, license='Apache License, Version 2.0', classifiers=[ diff --git a/sqlalchemydiff/comparer.py b/sqlalchemydiff/comparer.py index 57441c6..2fbc506 100644 --- a/sqlalchemydiff/comparer.py +++ b/sqlalchemydiff/comparer.py @@ -1,14 +1,15 @@ # -*- coding: utf-8 -*- from copy import deepcopy -from .util import TablesInfo, DiffResult, InspectorFactory, CompareResult +from .util import ( + TablesInfo, DiffResult, InspectorFactory, CompareResult, IgnoreManager +) -def compare(left_uri, right_uri, ignore_tables=None): +def compare(left_uri, right_uri, ignores=None, ignores_sep=None): """Compare two databases, given two URIs. - Compare two databases, given two URIs and a (possibly empty) set of - tables to ignore during the comparison. + Compare two databases, ignoring whatever is specified in `ignores`. The ``info`` dict has this structure:: @@ -63,26 +64,34 @@ def compare(left_uri, right_uri, ignore_tables=None): :param string left_uri: The URI for the first (left) database. :param string right_uri: The URI for the second (right) database. - :param set ignore_tables: - A set of string values to be excluded from both databases (if - present) when doing the comparison. String matching is case - sensitive. + :param iterable ignores: + A list of strings in the format: + * `table-name` + * `table-name.identifier.name` + + If a table name is specified, the whole table is excluded from + comparison. If a complete clause is specified, then only the + specified element is excluded from comparison. `identifier` is one + of (`col`, `pk`, `fk`, `idx`) and name is the name of the element + to be excluded from the comparison. + :param string ignores_sep: + Separator to be used to spilt the `ignores` clauses. :return: A :class:`~.util.CompareResult` object with ``info`` and ``errors`` dicts populated with the comparison result. """ - if ignore_tables is None: - ignore_tables = set() + ignore_manager = IgnoreManager(ignores, separator=ignores_sep) left_inspector, right_inspector = _get_inspectors(left_uri, right_uri) tables_info = _get_tables_info( - left_inspector, right_inspector, ignore_tables) + left_inspector, right_inspector, ignore_manager.ignore_tables) info = _get_info_dict(left_uri, right_uri, tables_info) info['tables_data'] = _get_tables_data( - tables_info.common, left_inspector, right_inspector) + tables_info.common, left_inspector, right_inspector, ignore_manager + ) errors = _compile_errors(info) result = _make_result(info, errors) @@ -157,32 +166,53 @@ def _get_info_dict(left_uri, right_uri, tables_info): return info -def _get_tables_data(tables_common, left_inspector, right_inspector): +def _get_tables_data( + tables_common, left_inspector, right_inspector, ignore_manager +): tables_data = {} for table_name in tables_common: table_data = _get_table_data( - left_inspector, right_inspector, table_name) + left_inspector, right_inspector, table_name, ignore_manager + ) tables_data[table_name] = table_data return tables_data -def _get_table_data(left_inspector, right_inspector, table_name): +def _get_table_data( + left_inspector, right_inspector, table_name, ignore_manager +): table_data = {} # foreign keys table_data['foreign_keys'] = _get_foreign_keys_info( - left_inspector, right_inspector, table_name) + left_inspector, + right_inspector, + table_name, + ignore_manager.get(table_name, 'fk') + ) table_data['primary_keys'] = _get_primary_keys_info( - left_inspector, right_inspector, table_name) + left_inspector, + right_inspector, + table_name, + ignore_manager.get(table_name, 'pk') + ) table_data['indexes'] = _get_indexes_info( - left_inspector, right_inspector, table_name) + left_inspector, + right_inspector, + table_name, + ignore_manager.get(table_name, 'idx') + ) table_data['columns'] = _get_columns_info( - left_inspector, right_inspector, table_name) + left_inspector, + right_inspector, + table_name, + ignore_manager.get(table_name, 'col') + ) return table_data @@ -225,10 +255,15 @@ def _diff_dicts(left, right): )._asdict() -def _get_foreign_keys_info(left_inspector, right_inspector, table_name): +def _get_foreign_keys_info( + left_inspector, right_inspector, table_name, ignores +): left_fk_list = _get_foreign_keys(left_inspector, table_name) right_fk_list = _get_foreign_keys(right_inspector, table_name) + left_fk_list = _discard_ignores_by_name(left_fk_list, ignores) + right_fk_list = _discard_ignores_by_name(right_fk_list, ignores) + # process into dict left_fk = dict((elem['name'], elem) for elem in left_fk_list) right_fk = dict((elem['name'], elem) for elem in right_fk_list) @@ -240,10 +275,15 @@ def _get_foreign_keys(inspector, table_name): return inspector.get_foreign_keys(table_name) -def _get_primary_keys_info(left_inspector, right_inspector, table_name): +def _get_primary_keys_info( + left_inspector, right_inspector, table_name, ignores +): left_pk_list = _get_primary_keys(left_inspector, table_name) right_pk_list = _get_primary_keys(right_inspector, table_name) + left_pk_list = _discard_ignores(left_pk_list, ignores) + right_pk_list = _discard_ignores(right_pk_list, ignores) + # process into dict left_pk = dict((elem, elem) for elem in left_pk_list) right_pk = dict((elem, elem) for elem in right_pk_list) @@ -255,10 +295,13 @@ def _get_primary_keys(inspector, table_name): return inspector.get_primary_keys(table_name) -def _get_indexes_info(left_inspector, right_inspector, table_name): +def _get_indexes_info(left_inspector, right_inspector, table_name, ignores): left_index_list = _get_indexes(left_inspector, table_name) right_index_list = _get_indexes(right_inspector, table_name) + left_index_list = _discard_ignores_by_name(left_index_list, ignores) + right_index_list = _discard_ignores_by_name(right_index_list, ignores) + # process into dict left_index = dict((elem['name'], elem) for elem in left_index_list) right_index = dict((elem['name'], elem) for elem in right_index_list) @@ -270,10 +313,13 @@ def _get_indexes(inspector, table_name): return inspector.get_indexes(table_name) -def _get_columns_info(left_inspector, right_inspector, table_name): +def _get_columns_info(left_inspector, right_inspector, table_name, ignores): left_columns_list = _get_columns(left_inspector, table_name) right_columns_list = _get_columns(right_inspector, table_name) + left_columns_list = _discard_ignores_by_name(left_columns_list, ignores) + right_columns_list = _discard_ignores_by_name(right_columns_list, ignores) + # process into dict left_columns = dict((elem['name'], elem) for elem in left_columns_list) right_columns = dict((elem['name'], elem) for elem in right_columns_list) @@ -289,6 +335,14 @@ def _get_columns(inspector, table_name): return inspector.get_columns(table_name) +def _discard_ignores_by_name(items, ignores): + return [item for item in items if item['name'] not in ignores] + + +def _discard_ignores(items, ignores): + return [item for item in items if item not in ignores] + + def _process_types(column_dict): for column in column_dict: column_dict[column]['type'] = _process_type( diff --git a/sqlalchemydiff/util.py b/sqlalchemydiff/util.py index 8297e45..6b2fc37 100644 --- a/sqlalchemydiff/util.py +++ b/sqlalchemydiff/util.py @@ -3,6 +3,7 @@ from uuid import uuid4 import json +import six from sqlalchemy import inspect, create_engine from sqlalchemy_utils import create_database, drop_database, database_exists @@ -103,3 +104,75 @@ def prepare_schema_from_models(uri, sqlalchemy_base): """Creates the database schema from the ``SQLAlchemy`` models. """ engine = create_engine(uri) sqlalchemy_base.metadata.create_all(engine) + + +class IgnoreManager: + + allowed_identifiers = ['pk', 'fk', 'idx', 'col'] + + def __init__(self, ignores, separator=None): + self.separator = separator or '.' + self.parse(ignores or []) + + def parse(self, ignores): + ignore, tables = {}, set() + + for data in ignores: + self.validate_type(data) + + if self.is_table_name(data): + tables.add(data.strip()) + else: + self.validate_clause(data) + table_name, identifier, name = self.fetch_data_items(data) + self.validate_items(table_name, identifier, name) + + ignore.setdefault( + table_name, {} + ).setdefault(identifier, []).append(name) + + self.__ignore = ignore + self.__tables = tables + + def is_table_name(self, data): + return data.count(self.separator) == 0 + + def validate_type(self, data): + if not isinstance(data, six.string_types): + raise TypeError('{} is not a string'.format(data)) + + def validate_clause(self, data): + if len(data.split(self.separator)) != 3: + raise ValueError( + '{} is not a well formed clause: table_name.identifier.name' + .format(data) + ) + + def fetch_data_items(self, data): + return [item.strip() for item in data.split(self.separator)] + + def validate_items(self, table_name, identifier, name): + if identifier not in self.allowed_identifiers: + raise ValueError( + '{} is invalid. It must be in {}'.format( + identifier, self.allowed_identifiers + ) + ) + + items = (table_name, identifier, name) + if not all(items): + raise ValueError( + '{} is not a well formed clause: table_name.identifier.name' + .format(self.separator.join(items)) + ) + + def get(self, table_name, identifier): + return self.__ignore.get(table_name, {}).get(identifier, []) + + @property + def ignore_tables(self): + return self.__tables.copy() + + @property + def ignore_data(self): + return self.__ignore.copy() diff --git a/test/conftest.py b/test/conftest.py new file mode 100644 index 0000000..f291e93 --- /dev/null +++ b/test/conftest.py @@ -0,0 +1,22 @@ +import os + +import pytest +import yaml + + +@pytest.fixture(scope="session") +def project_root(): + return os.path.dirname(os.path.dirname(__file__)) + + +@pytest.fixture(scope="session") +def test_config(project_root): + config_file = os.path.join(project_root, "config", "config.yaml") + with open(config_file) as stream: + config = yaml.load(stream.read()) + return config + + +@pytest.fixture(scope="session") +def db_uri(test_config): + return test_config['DB_URIS']['test'] diff --git a/test/endtoend/conftest.py b/test/endtoend/conftest.py deleted file mode 100644 index ace9860..0000000 --- a/test/endtoend/conftest.py +++ /dev/null @@ -1,7 +0,0 @@ -# -*- coding: utf-8 -*- -import pytest - - -@pytest.fixture(scope="module") -def db_uri(): - return "mysql+mysqlconnector://root:@localhost/sqlalchemydiff" diff --git a/test/endtoend/test_example.py b/test/endtoend/test_example.py index 2aa0881..e4e845f 100644 --- a/test/endtoend/test_example.py +++ b/test/endtoend/test_example.py @@ -284,3 +284,116 @@ def walk_dict(d, path): if not path: return d return walk_dict(d[path[0]], path[1:]) + + +@pytest.mark.usefixtures("new_db_left") +@pytest.mark.usefixtures("new_db_right") +def test_ignores(uri_left, uri_right): + # functionally the same of `test_errors_dict_catches_all_differences` + # but all errors are silenced with ignores + prepare_schema_from_models(uri_left, Base_left) + prepare_schema_from_models(uri_right, Base_right) + + ignores = [ + 'mobile_numbers', + 'phone_numbers', + 'companies.col.name', + 'companies.idx.name', + 'employees.fk.fk_employees_companies', + 'employees.fk.fk_emp_comp', + 'employees.idx.ix_employees_name', + 'employees.idx.fk_employees_companies', + 'employees.idx.name', + 'employees.idx.fk_emp_comp', + 'roles.col.name', + 'skills.col.slug', + 'skills.col.id', + 'skills.pk.slug', + 'skills.pk.id', + ] + + result = compare(uri_left, uri_right, ignores) + + assert result.is_match + + +@pytest.mark.usefixtures("new_db_left") +@pytest.mark.usefixtures("new_db_right") +def test_ignores_alternative_sep(uri_left, uri_right): + # functionally the same of `test_errors_dict_catches_all_differences` + # but all errors are silenced with ignores + prepare_schema_from_models(uri_left, Base_left) + prepare_schema_from_models(uri_right, Base_right) + + ignores = [ + 'mobile_numbers', + 'phone_numbers', + 'companies#col#name', + 'companies#idx#name', + 'employees#fk#fk_employees_companies', + 'employees#fk#fk_emp_comp', + 'employees#idx#ix_employees_name', + 'employees#idx#fk_employees_companies', + 'employees#idx#name', + 'employees#idx#fk_emp_comp', + 'roles#col#name', + 'skills#col#slug', + 'skills#col#id', + 'skills#pk#slug', + 'skills#pk#id', + ] + + result = compare(uri_left, uri_right, ignores=ignores, ignores_sep='#') + + assert result.is_match + + +@pytest.mark.usefixtures("new_db_left") +@pytest.mark.usefixtures("new_db_right") +@pytest.mark.parametrize('missing_ignore', [ + 'mobile_numbers', + 'phone_numbers', + 'companies.col.name', + 'companies.idx.name', + 'employees.fk.fk_employees_companies', + 'employees.fk.fk_emp_comp', + 'employees.idx.ix_employees_name', + 'employees.idx.fk_employees_companies', + 'employees.idx.name', + 'employees.idx.fk_emp_comp', + 'roles.col.name', + 'skills.col.slug', + 'skills.col.id', + 'skills.pk.slug', + 'skills.pk.id', +]) +def test_ignores_all_needed(uri_left, uri_right, missing_ignore): + # proves each ignore clause is needed + prepare_schema_from_models(uri_left, Base_left) + prepare_schema_from_models(uri_right, Base_right) + + ignores = [ + 'mobile_numbers', + 'phone_numbers', + 'companies.col.name', + 'companies.idx.name', + 'employees.fk.fk_employees_companies', + 'employees.fk.fk_emp_comp', + 'employees.idx.ix_employees_name', + 'employees.idx.fk_employees_companies', + 'employees.idx.name', + 'employees.idx.fk_emp_comp', + 'roles.col.name', + 'skills.col.slug', + 'skills.col.id', + 'skills.pk.slug', + 'skills.pk.id', + ] + + result = compare( + uri_left, + uri_right, + ignores=list(set(ignores) - set([missing_ignore])) + ) + + assert not result.is_match diff --git a/test/unit/test_comparer.py b/test/unit/test_comparer.py index a13cbb6..4002459 100644 --- a/test/unit/test_comparer.py +++ b/test/unit/test_comparer.py @@ -6,6 +6,8 @@ from sqlalchemydiff.comparer import ( _compile_errors, _diff_dicts, + _discard_ignores, + _discard_ignores_by_name, _get_columns, _get_columns_info, _get_common_tables, @@ -110,7 +112,7 @@ def test_compare_calls_chain( tables_info = _get_tables_info_mock.return_value result = compare( - "left_uri", "right_uri", ignore_tables=set(['ignore_me'])) + "left_uri", "right_uri", ignores=['ignore_me']) expected_info = { 'uris': { @@ -145,7 +147,7 @@ def test__get_tables_info_called_with_correct_inspectors( _get_tables_data_mock, _compile_errors_mock): left_inspector, right_inspector = _get_inspectors_mock.return_value - compare("left_uri", "right_uri", ignore_tables=set(['ignore_me'])) + compare("left_uri", "right_uri", ignores=['ignore_me']) _get_inspectors_mock.assert_called_once_with("left_uri", "right_uri") _get_tables_info_mock.assert_called_once_with( @@ -155,7 +157,7 @@ def test__get_tables_info_called_with_correct_inspectors( @pytest.mark.usefixtures("mock_inspector_factory") class TestCompareInternals(object): - ## FIXTURES + # FIXTURES @pytest.yield_fixture def _get_table_data_mock(self): @@ -217,7 +219,7 @@ def _get_columns_info_mock(self): with patch('sqlalchemydiff.comparer._get_columns_info') as m: yield m - ## TESTS + # TESTS def test__get_inspectors(self): left_inspector_mock, right_inspector_mock = Mock(), Mock() @@ -309,11 +311,13 @@ def test__get_tables_data(self, _get_table_data_mock): {'table_data': 'data_A'}, {'table_data': 'data_B'}, ] - left_inspector, right_inspector = Mock(), Mock() + left_inspector, right_inspector, ignore_manager = ( + Mock(), Mock(), Mock() + ) tables_common = ['common_table_A', 'common_table_B'] tables_data = _get_tables_data( - tables_common, left_inspector, right_inspector) + tables_common, left_inspector, right_inspector, ignore_manager) expected_tables_data = { 'common_table_A': {'table_data': 'data_A'}, @@ -321,6 +325,16 @@ def test__get_tables_data(self, _get_table_data_mock): } assert expected_tables_data == tables_data + assert [ + call( + left_inspector, right_inspector, 'common_table_A', + ignore_manager + ), + call( + left_inspector, right_inspector, 'common_table_B', + ignore_manager + ), + ] == _get_table_data_mock.call_args_list def test__make_result(self): info = {'info': 'dict'} @@ -369,7 +383,7 @@ def test__get_foreign_keys_info( left_inspector, right_inspector = Mock(), Mock() result = _get_foreign_keys_info( - left_inspector, right_inspector, 'table_A') + left_inspector, right_inspector, 'table_A', []) _diff_dicts_mock.assert_called_once_with( { @@ -383,6 +397,29 @@ def test__get_foreign_keys_info( assert _diff_dicts_mock.return_value == result + def test__get_foreign_keys_info_ignores( + self, _diff_dicts_mock, _get_foreign_keys_mock): + _get_foreign_keys_mock.side_effect = [ + [{'name': 'fk_left_1'}, {'name': 'fk_left_2'}], + [{'name': 'fk_right_1'}, {'name': 'fk_right_2'}] + ] + left_inspector, right_inspector = Mock(), Mock() + ignores = ['fk_left_1', 'fk_right_2'] + + result = _get_foreign_keys_info( + left_inspector, right_inspector, 'table_A', ignores) + + _diff_dicts_mock.assert_called_once_with( + { + 'fk_left_2': {'name': 'fk_left_2'} + }, + { + 'fk_right_1': {'name': 'fk_right_1'} + } + ) + + assert _diff_dicts_mock.return_value == result + def test__get_foreign_keys(self): inspector = Mock() @@ -400,7 +437,7 @@ def test__get_primary_keys_info( left_inspector, right_inspector = Mock(), Mock() result = _get_primary_keys_info( - left_inspector, right_inspector, 'table_A') + left_inspector, right_inspector, 'table_A', []) _diff_dicts_mock.assert_called_once_with( {'pk_left_1': 'pk_left_1', 'pk_left_2': 'pk_left_2'}, @@ -409,6 +446,25 @@ def test__get_primary_keys_info( assert _diff_dicts_mock.return_value == result + def test__get_primary_keys_info_ignores( + self, _diff_dicts_mock, _get_primary_keys_mock): + _get_primary_keys_mock.side_effect = [ + ['pk_left_1', 'pk_left_2'], + ['pk_right_1', 'pk_right_2'] + ] + left_inspector, right_inspector = Mock(), Mock() + ignores = ['pk_left_1', 'pk_right_2'] + + result = _get_primary_keys_info( + left_inspector, right_inspector, 'table_A', ignores) + + _diff_dicts_mock.assert_called_once_with( + {'pk_left_2': 'pk_left_2'}, + {'pk_right_1': 'pk_right_1'} + ) + + assert _diff_dicts_mock.return_value == result + def test__get_primary_keys(self): inspector = Mock() @@ -426,7 +482,7 @@ def test__get_indexes_info( left_inspector, right_inspector = Mock(), Mock() result = _get_indexes_info( - left_inspector, right_inspector, 'table_A') + left_inspector, right_inspector, 'table_A', []) _diff_dicts_mock.assert_called_once_with( { @@ -440,6 +496,29 @@ def test__get_indexes_info( assert _diff_dicts_mock.return_value == result + def test__get_indexes_info_ignores( + self, _diff_dicts_mock, _get_indexes_mock): + _get_indexes_mock.side_effect = [ + [{'name': 'index_left_1'}, {'name': 'index_left_2'}], + [{'name': 'index_right_1'}, {'name': 'index_right_2'}] + ] + left_inspector, right_inspector = Mock(), Mock() + ignores = ['index_left_1', 'index_right_2'] + + result = _get_indexes_info( + left_inspector, right_inspector, 'table_A', ignores) + + _diff_dicts_mock.assert_called_once_with( + { + 'index_left_2': {'name': 'index_left_2'} + }, + { + 'index_right_1': {'name': 'index_right_1'} + } + ) + + assert _diff_dicts_mock.return_value == result + def test__get_indexes(self): inspector = Mock() @@ -462,7 +541,7 @@ def process_types_side_effect(columns): left_inspector, right_inspector = Mock(), Mock() result = _get_columns_info( - left_inspector, right_inspector, 'table_A') + left_inspector, right_inspector, 'table_A', []) _diff_dicts_mock.assert_called_once_with( { @@ -478,6 +557,36 @@ def process_types_side_effect(columns): assert _diff_dicts_mock.return_value == result + def test__get_columns_info_ignores( + self, _diff_dicts_mock, _get_columns_mock, _process_types_mock): + _get_columns_mock.side_effect = [ + [{'name': 'columns_left_1'}, {'name': 'columns_left_2'}], + [{'name': 'columns_right_1'}, {'name': 'columns_right_2'}] + ] + + def process_types_side_effect(columns): + columns['_processed'] = True + _process_types_mock.side_effect = process_types_side_effect + + left_inspector, right_inspector = Mock(), Mock() + ignores = ['columns_left_1', 'columns_right_2'] + + result = _get_columns_info( + left_inspector, right_inspector, 'table_A', ignores) + + _diff_dicts_mock.assert_called_once_with( + { + '_processed': True, + 'columns_left_2': {'name': 'columns_left_2'} + }, + { + '_processed': True, + 'columns_right_1': {'name': 'columns_right_1'} + } + ) + + assert _diff_dicts_mock.return_value == result + def test__get_columns(self): inspector = Mock() @@ -523,7 +632,9 @@ def test__get_table_data( 'left_only': 13, 'right_only': 14, 'common': 15, 'diff': 16 } - result = _get_table_data(left_inspector, right_inspector, 'table_A') + result = _get_table_data( + left_inspector, right_inspector, 'table_A', Mock() + ) expected_result = { 'foreign_keys': { @@ -734,3 +845,21 @@ def test__compile_errors_without_errors(self): errors = _compile_errors(info) assert expected_errors == errors + + @pytest.mark.parametrize('ignores,expected', [ + ([], [{'name': 'A'}, {'name': 'B'}, {'name': 'C'}]), + (['A', 'C'], [{'name': 'B'}]), + ]) + def test__discard_ignores_by_name(self, ignores, expected): + items = [{'name': 'A'}, {'name': 'B'}, {'name': 'C'}] + + assert expected == _discard_ignores_by_name(items, ignores) + + @pytest.mark.parametrize('ignores,expected', [ + ([], ['A', 'B', 'C']), + (['A', 'C'], ['B']), + ]) + def test__discard_ignores(self, ignores, expected): + items = ['A', 'B', 'C'] + + assert expected == _discard_ignores(items, ignores) diff --git a/test/unit/test_util.py b/test/unit/test_util.py index 69d2caf..25b2ffc 100644 --- a/test/unit/test_util.py +++ b/test/unit/test_util.py @@ -5,8 +5,8 @@ import pytest -from sqlalchemydiff.util import CompareResult, InspectorFactory -from mock import Mock, patch, call +from sqlalchemydiff.util import CompareResult, InspectorFactory, IgnoreManager +from mock import Mock, patch class TestCompareResult(object): @@ -23,10 +23,10 @@ def test_is_match(self): info, errors = {}, {} result = CompareResult(info, errors) - assert True == result.is_match + assert result.is_match result.errors = {1: 1} - assert False == result.is_match + assert not result.is_match def test_dump_info(self): info = {'some': 'info'} @@ -73,3 +73,201 @@ def test_from_uri(self, inspect_mock, create_engine_mock): inspect_mock.assert_called_once_with(create_engine_mock.return_value) assert inspect_mock.return_value == inspector + + +class TestIgnoreManager: + + @pytest.fixture + def ignore_data(self): + return [ + 'table-A.pk.id', + 'table-A.fk.user_id', + 'table-A.fk.address_id', + 'table-B.pk.root_id', + 'table-C.col.telephone', + 'table-C.idx.admin_id', + 'table-D', + 'table-E', + ] + + @pytest.mark.parametrize('ignore', [ + None, + [], + (), + ]) + def test_init_empty(self, ignore): + im = IgnoreManager(ignore) + + assert {} == im.ignore_data + assert set() == im.ignore_tables + + def test_init(self, ignore_data): + im = IgnoreManager(ignore_data) + + expected_ignore = { + 'table-A': { + 'pk': ['id'], + 'fk': ['user_id', 'address_id'], + }, + 'table-B': { + 'pk': ['root_id'], + }, + 'table-C': { + 'col': ['telephone'], + 'idx': ['admin_id'], + }, + } + + expected_tables = set(['table-D', 'table-E']) + + assert expected_ignore == im.ignore_data + assert expected_tables == im.ignore_tables + + def test_init_alternative_separator(self, ignore_data): + ignore_data = [clause.replace('.', '#') for clause in ignore_data] + im = IgnoreManager(ignore_data, separator='#') + + expected_ignore = { + 'table-A': { + 'pk': ['id'], + 'fk': ['user_id', 'address_id'], + }, + 'table-B': { + 'pk': ['root_id'], + }, + 'table-C': { + 'col': ['telephone'], + 'idx': ['admin_id'], + }, + } + + expected_tables = set(['table-D', 'table-E']) + + assert expected_ignore == im.ignore_data + assert expected_tables == im.ignore_tables + + def test_ignore_tables_property(self, ignore_data): + im = IgnoreManager(ignore_data) + + expected_tables = set(['table-D', 'table-E']) + + assert expected_tables == im.ignore_tables + + # make sure the property returns a copy + im.ignore_tables.add('another-table') + assert expected_tables == im.ignore_tables + + def test_ignore_data_property(self, ignore_data): + im = IgnoreManager(ignore_data) + + expected_ignore = { + 'table-A': { + 'pk': ['id'], + 'fk': ['user_id', 'address_id'], + }, + 'table-B': { + 'pk': ['root_id'], + }, + 'table-C': { + 'col': ['telephone'], + 'idx': ['admin_id'], + }, + } + + assert expected_ignore == im.ignore_data + + # make sure the property returns a copy + im.ignore_data['another-table'] = {'something': 'else'} + assert expected_ignore == im.ignore_data + + def test_init_strip(self): + ignore_data = [' table-A . pk . id ', ' table-C '] + + im = IgnoreManager(ignore_data) + + expected_ignore = { + 'table-A': { + 'pk': ['id'] + } + } + + expected_tables = set(['table-C']) + + assert expected_ignore == im.ignore_data + assert expected_tables == im.ignore_tables + + def test_identifier_incorrect(self): + ignore_data = ['table-A.unknown.some-name'] + + with pytest.raises(ValueError) as err: + IgnoreManager(ignore_data) + + assert ( + "unknown is invalid. It must be in ['pk', 'fk', 'idx', 'col']", + ) == err.value.args + + @pytest.mark.parametrize('clause', [ + 'too.few', + 'too.many.definitely.for-sure', + ]) + def test_incorrect_clause(self, clause): + ignore_data = [clause] + + with pytest.raises(ValueError) as err: + IgnoreManager(ignore_data) + + assert ( + '{} is not a well formed clause: table_name.identifier.name' + .format(clause), + ) == err.value.args + + @pytest.mark.parametrize('clause', [ + '.pk.b', + 'a.pk.', + ]) + def test_incorrect_empty_clause(self, clause): + ignore_data = [clause] + + with pytest.raises(ValueError) as err: + IgnoreManager(ignore_data) + + assert ( + '{} is not a well formed clause: table_name.identifier.name' + .format(clause), + ) == err.value.args + + @pytest.mark.parametrize('clause', [ + 3, + 3.14159265, + [], + (), + {}, + None, + ]) + def test_type_error_clause(self, clause): + ignore_data = [clause] + + with pytest.raises(TypeError) as err: + IgnoreManager(ignore_data) + + assert ( + '{} is not a string'.format(clause), + ) == err.value.args + + def test_get_missing_table(self): + ignore_data = [] + + im = IgnoreManager(ignore_data) + + assert [] == im.get('some-table-name', 'some-identifier') + + def test_get_missing_identifier(self, ignore_data): + im = IgnoreManager(ignore_data) + + assert [] == im.get('table-C', 'pk') + + def test_get(self, ignore_data): + im = IgnoreManager(ignore_data) + + assert ['id'] == im.get('table-A', 'pk') + assert ['user_id', 'address_id'] == im.get('table-A', 'fk')