Skip to content

Commit 63fe8b9

Browse files
committed
Merge pull request #3 from Overseas-Student-Living/PLAT-928
Plat 928
2 parents 3f5d729 + 8595060 commit 63fe8b9

File tree

12 files changed

+653
-55
lines changed

12 files changed

+653
-55
lines changed

config/config.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
DB_URIS:
2+
"test": "mysql+mysqlconnector://root:password@dockermachine:3306/sqlalchemydiff"
-271 KB
Binary file not shown.

docs/source/conf.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,9 @@
6161
# built documents.
6262
#
6363
# The short X.Y version.
64-
version = '0.0.1'
64+
version = '0.1.0'
6565
# The full version, including alpha/beta/rc tags.
66-
release = '0.0.1'
66+
release = '0.1.0'
6767

6868
# The language for content autogenerated by Sphinx. Refer to documentation
6969
# for a list of supported languages.

docs/source/index.rst

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,20 @@ them by looking at the ``errors`` dict on the ``result``:
7777
}
7878
7979
80+
You can also tell the comparer to ignore parts of the database.
81+
82+
For example, to ignore the ``users`` table completely, and the ``line3`` column
83+
of the table ``addresses`` you would call ``compare`` like this:
84+
85+
.. code-block:: Python
86+
87+
>>> result = compare(
88+
uri_left,
89+
uri_right,
90+
ignores=['users', 'addresses.col.line3']
91+
)
92+
93+
8094
If you wish to persist that dict to a JSON file, you can quickly do so
8195
by calling ``result.dump_errors()``.
8296

@@ -91,6 +105,8 @@ Currently the library can detect the following differences:
91105
- Differences in **Foreign Keys** for a common table
92106
- Differences in **Indexes** for a common table
93107
- Differences in **Columns** for a common table
108+
- Ability to ignore a **whole table**
109+
- Ability to ignore **primary/foreign keys**, **indexes** and **columns**
94110

95111

96112
Installation

setup.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
setup(
1515
name='sqlalchemy-diff',
16-
version='0.0.4',
16+
version='0.1.0',
1717
description='Compare two database schemas using sqlalchemy.',
1818
long_description=readme,
1919
author='student.com',
@@ -22,21 +22,19 @@
2222
packages=find_packages(exclude=['docs', 'test', 'test.*']),
2323
install_requires=[
2424
"six==1.10.0",
25-
"mock>=1.3.0",
26-
"sqlalchemy-utils==0.31.2",
25+
"mock>=2.0.0",
26+
"sqlalchemy-utils==0.32.4",
27+
"pyyaml==3.11",
2728
],
2829
extras_require={
2930
'dev': [
3031
"mysql-connector-python==2.0.4",
31-
"pytest==2.8.2",
32+
"pytest==2.9.1",
3233
],
3334
'docs': [
34-
"Sphinx==1.3.1",
35+
"sphinx==1.4.1",
3536
],
3637
},
37-
dependency_links=[
38-
"dists/mysql-connector-python-2.0.4.zip",
39-
],
4038
zip_safe=True,
4139
license='Apache License, Version 2.0',
4240
classifiers=[

sqlalchemydiff/comparer.py

Lines changed: 77 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
# -*- coding: utf-8 -*-
22
from copy import deepcopy
33

4-
from .util import TablesInfo, DiffResult, InspectorFactory, CompareResult
4+
from .util import (
5+
TablesInfo, DiffResult, InspectorFactory, CompareResult, IgnoreManager
6+
)
57

68

7-
def compare(left_uri, right_uri, ignore_tables=None):
9+
def compare(left_uri, right_uri, ignores=None, ignores_sep=None):
810
"""Compare two databases, given two URIs.
911
10-
Compare two databases, given two URIs and a (possibly empty) set of
11-
tables to ignore during the comparison.
12+
Compare two databases, ignoring whatever is specified in `ignores`.
1213
1314
The ``info`` dict has this structure::
1415
@@ -63,26 +64,34 @@ def compare(left_uri, right_uri, ignore_tables=None):
6364
6465
:param string left_uri: The URI for the first (left) database.
6566
:param string right_uri: The URI for the second (right) database.
66-
:param set ignore_tables:
67-
A set of string values to be excluded from both databases (if
68-
present) when doing the comparison. String matching is case
69-
sensitive.
67+
:param iterable ignores:
68+
A list of strings in the format:
69+
* `table-name`
70+
* `table-name.identifier.name`
71+
72+
If a table name is specified, the whole table is excluded from
73+
comparison. If a complete clause is specified, then only the
74+
specified element is excluded from comparison. `identifier` is one
75+
of (`col`, `pk`, `fk`, `idx`) and name is the name of the element
76+
to be excluded from the comparison.
77+
:param string ignores_sep:
78+
Separator to be used to spilt the `ignores` clauses.
7079
:return:
7180
A :class:`~.util.CompareResult` object with ``info`` and
7281
``errors`` dicts populated with the comparison result.
7382
"""
74-
if ignore_tables is None:
75-
ignore_tables = set()
83+
ignore_manager = IgnoreManager(ignores, separator=ignores_sep)
7684

7785
left_inspector, right_inspector = _get_inspectors(left_uri, right_uri)
7886

7987
tables_info = _get_tables_info(
80-
left_inspector, right_inspector, ignore_tables)
88+
left_inspector, right_inspector, ignore_manager.ignore_tables)
8189

8290
info = _get_info_dict(left_uri, right_uri, tables_info)
8391

8492
info['tables_data'] = _get_tables_data(
85-
tables_info.common, left_inspector, right_inspector)
93+
tables_info.common, left_inspector, right_inspector, ignore_manager
94+
)
8695

8796
errors = _compile_errors(info)
8897
result = _make_result(info, errors)
@@ -157,32 +166,53 @@ def _get_info_dict(left_uri, right_uri, tables_info):
157166
return info
158167

159168

160-
def _get_tables_data(tables_common, left_inspector, right_inspector):
169+
def _get_tables_data(
170+
tables_common, left_inspector, right_inspector, ignore_manager
171+
):
161172
tables_data = {}
162173

163174
for table_name in tables_common:
164175
table_data = _get_table_data(
165-
left_inspector, right_inspector, table_name)
176+
left_inspector, right_inspector, table_name, ignore_manager
177+
)
166178
tables_data[table_name] = table_data
167179

168180
return tables_data
169181

170182

171-
def _get_table_data(left_inspector, right_inspector, table_name):
183+
def _get_table_data(
184+
left_inspector, right_inspector, table_name, ignore_manager
185+
):
172186
table_data = {}
173187

174188
# foreign keys
175189
table_data['foreign_keys'] = _get_foreign_keys_info(
176-
left_inspector, right_inspector, table_name)
190+
left_inspector,
191+
right_inspector,
192+
table_name,
193+
ignore_manager.get(table_name, 'fk')
194+
)
177195

178196
table_data['primary_keys'] = _get_primary_keys_info(
179-
left_inspector, right_inspector, table_name)
197+
left_inspector,
198+
right_inspector,
199+
table_name,
200+
ignore_manager.get(table_name, 'pk')
201+
)
180202

181203
table_data['indexes'] = _get_indexes_info(
182-
left_inspector, right_inspector, table_name)
204+
left_inspector,
205+
right_inspector,
206+
table_name,
207+
ignore_manager.get(table_name, 'idx')
208+
)
183209

184210
table_data['columns'] = _get_columns_info(
185-
left_inspector, right_inspector, table_name)
211+
left_inspector,
212+
right_inspector,
213+
table_name,
214+
ignore_manager.get(table_name, 'col')
215+
)
186216

187217
return table_data
188218

@@ -225,10 +255,15 @@ def _diff_dicts(left, right):
225255
)._asdict()
226256

227257

228-
def _get_foreign_keys_info(left_inspector, right_inspector, table_name):
258+
def _get_foreign_keys_info(
259+
left_inspector, right_inspector, table_name, ignores
260+
):
229261
left_fk_list = _get_foreign_keys(left_inspector, table_name)
230262
right_fk_list = _get_foreign_keys(right_inspector, table_name)
231263

264+
left_fk_list = _discard_ignores_by_name(left_fk_list, ignores)
265+
right_fk_list = _discard_ignores_by_name(right_fk_list, ignores)
266+
232267
# process into dict
233268
left_fk = dict((elem['name'], elem) for elem in left_fk_list)
234269
right_fk = dict((elem['name'], elem) for elem in right_fk_list)
@@ -240,10 +275,15 @@ def _get_foreign_keys(inspector, table_name):
240275
return inspector.get_foreign_keys(table_name)
241276

242277

243-
def _get_primary_keys_info(left_inspector, right_inspector, table_name):
278+
def _get_primary_keys_info(
279+
left_inspector, right_inspector, table_name, ignores
280+
):
244281
left_pk_list = _get_primary_keys(left_inspector, table_name)
245282
right_pk_list = _get_primary_keys(right_inspector, table_name)
246283

284+
left_pk_list = _discard_ignores(left_pk_list, ignores)
285+
right_pk_list = _discard_ignores(right_pk_list, ignores)
286+
247287
# process into dict
248288
left_pk = dict((elem, elem) for elem in left_pk_list)
249289
right_pk = dict((elem, elem) for elem in right_pk_list)
@@ -255,10 +295,13 @@ def _get_primary_keys(inspector, table_name):
255295
return inspector.get_primary_keys(table_name)
256296

257297

258-
def _get_indexes_info(left_inspector, right_inspector, table_name):
298+
def _get_indexes_info(left_inspector, right_inspector, table_name, ignores):
259299
left_index_list = _get_indexes(left_inspector, table_name)
260300
right_index_list = _get_indexes(right_inspector, table_name)
261301

302+
left_index_list = _discard_ignores_by_name(left_index_list, ignores)
303+
right_index_list = _discard_ignores_by_name(right_index_list, ignores)
304+
262305
# process into dict
263306
left_index = dict((elem['name'], elem) for elem in left_index_list)
264307
right_index = dict((elem['name'], elem) for elem in right_index_list)
@@ -270,10 +313,13 @@ def _get_indexes(inspector, table_name):
270313
return inspector.get_indexes(table_name)
271314

272315

273-
def _get_columns_info(left_inspector, right_inspector, table_name):
316+
def _get_columns_info(left_inspector, right_inspector, table_name, ignores):
274317
left_columns_list = _get_columns(left_inspector, table_name)
275318
right_columns_list = _get_columns(right_inspector, table_name)
276319

320+
left_columns_list = _discard_ignores_by_name(left_columns_list, ignores)
321+
right_columns_list = _discard_ignores_by_name(right_columns_list, ignores)
322+
277323
# process into dict
278324
left_columns = dict((elem['name'], elem) for elem in left_columns_list)
279325
right_columns = dict((elem['name'], elem) for elem in right_columns_list)
@@ -289,6 +335,14 @@ def _get_columns(inspector, table_name):
289335
return inspector.get_columns(table_name)
290336

291337

338+
def _discard_ignores_by_name(items, ignores):
339+
return [item for item in items if item['name'] not in ignores]
340+
341+
342+
def _discard_ignores(items, ignores):
343+
return [item for item in items if item not in ignores]
344+
345+
292346
def _process_types(column_dict):
293347
for column in column_dict:
294348
column_dict[column]['type'] = _process_type(

sqlalchemydiff/util.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from uuid import uuid4
44
import json
55

6+
import six
67
from sqlalchemy import inspect, create_engine
78
from sqlalchemy_utils import create_database, drop_database, database_exists
89

@@ -103,3 +104,75 @@ def prepare_schema_from_models(uri, sqlalchemy_base):
103104
"""Creates the database schema from the ``SQLAlchemy`` models. """
104105
engine = create_engine(uri)
105106
sqlalchemy_base.metadata.create_all(engine)
107+
108+
109+
class IgnoreManager:
110+
111+
allowed_identifiers = ['pk', 'fk', 'idx', 'col']
112+
113+
def __init__(self, ignores, separator=None):
114+
self.separator = separator or '.'
115+
self.parse(ignores or [])
116+
117+
def parse(self, ignores):
118+
ignore, tables = {}, set()
119+
120+
for data in ignores:
121+
self.validate_type(data)
122+
123+
if self.is_table_name(data):
124+
tables.add(data.strip())
125+
else:
126+
self.validate_clause(data)
127+
table_name, identifier, name = self.fetch_data_items(data)
128+
self.validate_items(table_name, identifier, name)
129+
130+
ignore.setdefault(
131+
table_name, {}
132+
).setdefault(identifier, []).append(name)
133+
134+
self.__ignore = ignore
135+
self.__tables = tables
136+
137+
def is_table_name(self, data):
138+
return data.count(self.separator) == 0
139+
140+
def validate_type(self, data):
141+
if not isinstance(data, six.string_types):
142+
raise TypeError('{} is not a string'.format(data))
143+
144+
def validate_clause(self, data):
145+
if len(data.split(self.separator)) != 3:
146+
raise ValueError(
147+
'{} is not a well formed clause: table_name.identifier.name'
148+
.format(data)
149+
)
150+
151+
def fetch_data_items(self, data):
152+
return [item.strip() for item in data.split(self.separator)]
153+
154+
def validate_items(self, table_name, identifier, name):
155+
if identifier not in self.allowed_identifiers:
156+
raise ValueError(
157+
'{} is invalid. It must be in {}'.format(
158+
identifier, self.allowed_identifiers
159+
)
160+
)
161+
162+
items = (table_name, identifier, name)
163+
if not all(items):
164+
raise ValueError(
165+
'{} is not a well formed clause: table_name.identifier.name'
166+
.format(self.separator.join(items))
167+
)
168+
169+
def get(self, table_name, identifier):
170+
return self.__ignore.get(table_name, {}).get(identifier, [])
171+
172+
@property
173+
def ignore_tables(self):
174+
return self.__tables.copy()
175+
176+
@property
177+
def ignore_data(self):
178+
return self.__ignore.copy()

test/conftest.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import os
2+
3+
import pytest
4+
import yaml
5+
6+
7+
@pytest.fixture(scope="session")
8+
def project_root():
9+
return os.path.dirname(os.path.dirname(__file__))
10+
11+
12+
@pytest.fixture(scope="session")
13+
def test_config(project_root):
14+
config_file = os.path.join(project_root, "config", "config.yaml")
15+
with open(config_file) as stream:
16+
config = yaml.load(stream.read())
17+
return config
18+
19+
20+
@pytest.fixture(scope="session")
21+
def db_uri(test_config):
22+
return test_config['DB_URIS']['test']

test/endtoend/conftest.py

Lines changed: 0 additions & 7 deletions
This file was deleted.

0 commit comments

Comments
 (0)