|
| 1 | +# -*- coding: utf-8 -*- |
| 2 | +from copy import deepcopy |
| 3 | + |
| 4 | +from .util import TablesInfo, DiffResult, InspectorFactory, CompareResult |
| 5 | + |
| 6 | + |
| 7 | +def compare(left_uri, right_uri, ignore_tables=None): |
| 8 | + """Compare two databases, given two URIs. |
| 9 | +
|
| 10 | + Compare two databases, given two URIs and a (possibly empty) set of |
| 11 | + tables to ignore during the comparison. |
| 12 | +
|
| 13 | + The ``info`` dict has this structure:: |
| 14 | +
|
| 15 | + info = { |
| 16 | + 'uris': { |
| 17 | + 'left': 'left_uri', |
| 18 | + 'right': 'right_uri', |
| 19 | + }, |
| 20 | + 'tables': { |
| 21 | + 'left': 'tables_left', |
| 22 | + 'left_only': 'tables_left_only', |
| 23 | + 'right': 'tables_right', |
| 24 | + 'right_only': 'tables_right_only', |
| 25 | + 'common': ['table_name_1', 'table_name_2'], |
| 26 | + }, |
| 27 | + 'tables_data': { |
| 28 | +
|
| 29 | + 'table_name_1': { |
| 30 | + 'foreign_keys': { |
| 31 | + 'left_only': [...], |
| 32 | + 'right_only': [...], |
| 33 | + 'common': [...], |
| 34 | + 'diff': [...], |
| 35 | + }, |
| 36 | + 'primary_keys': { |
| 37 | + 'left_only': [...], |
| 38 | + 'right_only': [...], |
| 39 | + 'common': [...], |
| 40 | + 'diff': [...], |
| 41 | + }, |
| 42 | + 'indexes': { |
| 43 | + 'left_only': [...], |
| 44 | + 'right_only': [...], |
| 45 | + 'common': [...], |
| 46 | + 'diff': [...], |
| 47 | + }, |
| 48 | + 'columns': { |
| 49 | + 'left_only': [...], |
| 50 | + 'right_only': [...], |
| 51 | + 'common': [...], |
| 52 | + 'diff': [...], |
| 53 | + } |
| 54 | + }, |
| 55 | +
|
| 56 | + 'table_name_2': { ... }, |
| 57 | + } |
| 58 | + } |
| 59 | +
|
| 60 | + The ``errors`` dict will follow the same structure of the ``info`` |
| 61 | + dict, but it will only have the data that is showing a discrepancy |
| 62 | + between the two databases. |
| 63 | +
|
| 64 | + :param string left_uri: The URI for the first (left) database. |
| 65 | + :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. |
| 70 | + :return: |
| 71 | + A :class:`~.util.CompareResult` object with ``info`` and |
| 72 | + ``errors`` dicts populated with the comparison result. |
| 73 | + """ |
| 74 | + if ignore_tables is None: |
| 75 | + ignore_tables = set() |
| 76 | + |
| 77 | + left_inspector, right_inspector = _get_inspectors(left_uri, right_uri) |
| 78 | + |
| 79 | + tables_info = _get_tables_info( |
| 80 | + left_inspector, right_inspector, ignore_tables) |
| 81 | + |
| 82 | + info = _get_info_dict(left_uri, right_uri, tables_info) |
| 83 | + |
| 84 | + info['tables_data'] = _get_tables_data( |
| 85 | + tables_info.common, left_inspector, right_inspector) |
| 86 | + |
| 87 | + errors = _compile_errors(info) |
| 88 | + result = _make_result(info, errors) |
| 89 | + |
| 90 | + return result |
| 91 | + |
| 92 | + |
| 93 | +def _get_inspectors(left_uri, right_uri): |
| 94 | + left_inspector = InspectorFactory.from_uri(left_uri) |
| 95 | + right_inspector = InspectorFactory.from_uri(right_uri) |
| 96 | + return left_inspector, right_inspector |
| 97 | + |
| 98 | + |
| 99 | +def _get_tables_info(left_inspector, right_inspector, ignore_tables): |
| 100 | + """Get information about the differences at the table level. """ |
| 101 | + tables_left, tables_right = _get_tables( |
| 102 | + left_inspector, right_inspector, ignore_tables) |
| 103 | + |
| 104 | + tables_left_only, tables_right_only = _get_tables_diff( |
| 105 | + tables_left, tables_right) |
| 106 | + |
| 107 | + tables_common = _get_common_tables(tables_left, tables_right) |
| 108 | + |
| 109 | + return TablesInfo( |
| 110 | + left=tables_left, right=tables_right, left_only=tables_left_only, |
| 111 | + right_only=tables_right_only, common=tables_common) |
| 112 | + |
| 113 | + |
| 114 | +def _get_tables(left_inspector, right_inspector, ignore_tables): |
| 115 | + """Get table names for both databases. ``ignore_tables`` are removed. """ |
| 116 | + tables_left = _get_tables_names(left_inspector, ignore_tables) |
| 117 | + tables_right = _get_tables_names(right_inspector, ignore_tables) |
| 118 | + return tables_left, tables_right |
| 119 | + |
| 120 | + |
| 121 | +def _get_tables_names(inspector, ignore_tables): |
| 122 | + return sorted(set(inspector.get_table_names()) - ignore_tables) |
| 123 | + |
| 124 | + |
| 125 | +def _get_tables_diff(tables_left, tables_right): |
| 126 | + return ( |
| 127 | + _diff_table_lists(tables_left, tables_right), |
| 128 | + _diff_table_lists(tables_right, tables_left) |
| 129 | + ) |
| 130 | + |
| 131 | + |
| 132 | +def _diff_table_lists(tables_left, tables_right): |
| 133 | + return sorted(set(tables_left) - set(tables_right)) |
| 134 | + |
| 135 | + |
| 136 | +def _get_common_tables(tables_left, tables_right): |
| 137 | + return sorted(set(tables_left) & set(tables_right)) |
| 138 | + |
| 139 | + |
| 140 | +def _get_info_dict(left_uri, right_uri, tables_info): |
| 141 | + """Create an empty stub for the `info` dict. """ |
| 142 | + info = { |
| 143 | + 'uris': { |
| 144 | + 'left': left_uri, |
| 145 | + 'right': right_uri, |
| 146 | + }, |
| 147 | + 'tables': { |
| 148 | + 'left': tables_info.left, |
| 149 | + 'left_only': tables_info.left_only, |
| 150 | + 'right': tables_info.right, |
| 151 | + 'right_only': tables_info.right_only, |
| 152 | + 'common': tables_info.common, |
| 153 | + }, |
| 154 | + 'tables_data': {}, |
| 155 | + } |
| 156 | + |
| 157 | + return info |
| 158 | + |
| 159 | + |
| 160 | +def _get_tables_data(tables_common, left_inspector, right_inspector): |
| 161 | + tables_data = {} |
| 162 | + |
| 163 | + for table_name in tables_common: |
| 164 | + table_data = _get_table_data( |
| 165 | + left_inspector, right_inspector, table_name) |
| 166 | + tables_data[table_name] = table_data |
| 167 | + |
| 168 | + return tables_data |
| 169 | + |
| 170 | + |
| 171 | +def _get_table_data(left_inspector, right_inspector, table_name): |
| 172 | + table_data = {} |
| 173 | + |
| 174 | + # foreign keys |
| 175 | + table_data['foreign_keys'] = _get_foreign_keys_info( |
| 176 | + left_inspector, right_inspector, table_name) |
| 177 | + |
| 178 | + table_data['primary_keys'] = _get_primary_keys_info( |
| 179 | + left_inspector, right_inspector, table_name) |
| 180 | + |
| 181 | + table_data['indexes'] = _get_indexes_info( |
| 182 | + left_inspector, right_inspector, table_name) |
| 183 | + |
| 184 | + table_data['columns'] = _get_columns_info( |
| 185 | + left_inspector, right_inspector, table_name) |
| 186 | + |
| 187 | + return table_data |
| 188 | + |
| 189 | + |
| 190 | +def _diff_dicts(left, right): |
| 191 | + """Makes the diff of two dictionaries, based on keys and values. |
| 192 | +
|
| 193 | + :return: |
| 194 | + A 4-tuple with elements:: |
| 195 | +
|
| 196 | + * A list of elements only in left |
| 197 | + * A list of elements only in right |
| 198 | + * A list of common elements |
| 199 | + * A list of diff elements |
| 200 | + {'key':..., 'left':..., 'right':...} |
| 201 | + """ |
| 202 | + left_only_key = set(left) - set(right) |
| 203 | + right_only_key = set(right) - set(left) |
| 204 | + |
| 205 | + left_only = [left[key] for key in left_only_key] |
| 206 | + right_only = [right[key] for key in right_only_key] |
| 207 | + |
| 208 | + # common and diff |
| 209 | + common_keys = set(left) & set(right) |
| 210 | + common = [] |
| 211 | + diff = [] |
| 212 | + |
| 213 | + for key in common_keys: |
| 214 | + if left[key] == right[key]: |
| 215 | + common.append(left[key]) |
| 216 | + else: |
| 217 | + diff.append({ |
| 218 | + 'key': key, |
| 219 | + 'left': left[key], |
| 220 | + 'right': right[key], |
| 221 | + }) |
| 222 | + |
| 223 | + return DiffResult( |
| 224 | + left_only=left_only, right_only=right_only, common=common, diff=diff |
| 225 | + )._asdict() |
| 226 | + |
| 227 | + |
| 228 | +def _get_foreign_keys_info(left_inspector, right_inspector, table_name): |
| 229 | + left_fk_list = _get_foreign_keys(left_inspector, table_name) |
| 230 | + right_fk_list = _get_foreign_keys(right_inspector, table_name) |
| 231 | + |
| 232 | + # process into dict |
| 233 | + left_fk = dict((elem['name'], elem) for elem in left_fk_list) |
| 234 | + right_fk = dict((elem['name'], elem) for elem in right_fk_list) |
| 235 | + |
| 236 | + return _diff_dicts(left_fk, right_fk) |
| 237 | + |
| 238 | + |
| 239 | +def _get_foreign_keys(inspector, table_name): |
| 240 | + return inspector.get_foreign_keys(table_name) |
| 241 | + |
| 242 | + |
| 243 | +def _get_primary_keys_info(left_inspector, right_inspector, table_name): |
| 244 | + left_pk_list = _get_primary_keys(left_inspector, table_name) |
| 245 | + right_pk_list = _get_primary_keys(right_inspector, table_name) |
| 246 | + |
| 247 | + # process into dict |
| 248 | + left_pk = dict((elem, elem) for elem in left_pk_list) |
| 249 | + right_pk = dict((elem, elem) for elem in right_pk_list) |
| 250 | + |
| 251 | + return _diff_dicts(left_pk, right_pk) |
| 252 | + |
| 253 | + |
| 254 | +def _get_primary_keys(inspector, table_name): |
| 255 | + return inspector.get_primary_keys(table_name) |
| 256 | + |
| 257 | + |
| 258 | +def _get_indexes_info(left_inspector, right_inspector, table_name): |
| 259 | + left_index_list = _get_indexes(left_inspector, table_name) |
| 260 | + right_index_list = _get_indexes(right_inspector, table_name) |
| 261 | + |
| 262 | + # process into dict |
| 263 | + left_index = dict((elem['name'], elem) for elem in left_index_list) |
| 264 | + right_index = dict((elem['name'], elem) for elem in right_index_list) |
| 265 | + |
| 266 | + return _diff_dicts(left_index, right_index) |
| 267 | + |
| 268 | + |
| 269 | +def _get_indexes(inspector, table_name): |
| 270 | + return inspector.get_indexes(table_name) |
| 271 | + |
| 272 | + |
| 273 | +def _get_columns_info(left_inspector, right_inspector, table_name): |
| 274 | + left_columns_list = _get_columns(left_inspector, table_name) |
| 275 | + right_columns_list = _get_columns(right_inspector, table_name) |
| 276 | + |
| 277 | + # process into dict |
| 278 | + left_columns = dict((elem['name'], elem) for elem in left_columns_list) |
| 279 | + right_columns = dict((elem['name'], elem) for elem in right_columns_list) |
| 280 | + |
| 281 | + # process `type` fields |
| 282 | + _process_types(left_columns) |
| 283 | + _process_types(right_columns) |
| 284 | + |
| 285 | + return _diff_dicts(left_columns, right_columns) |
| 286 | + |
| 287 | + |
| 288 | +def _get_columns(inspector, table_name): |
| 289 | + return inspector.get_columns(table_name) |
| 290 | + |
| 291 | + |
| 292 | +def _process_types(column_dict): |
| 293 | + for column in column_dict: |
| 294 | + column_dict[column]['type'] = _process_type( |
| 295 | + column_dict[column]['type']) |
| 296 | + |
| 297 | + |
| 298 | +def _process_type(type_): |
| 299 | + """Process the SQLAlchemy Column Type ``type_``. |
| 300 | +
|
| 301 | + Calls :meth:`sqlalchemy.sql.type_api.TypeEngine.compile` on |
| 302 | + ``type_`` to produce a string-compiled form of it. "string-compiled" |
| 303 | + meaning as it would be used for a SQL clause. |
| 304 | + """ |
| 305 | + return type_.compile() |
| 306 | + |
| 307 | + |
| 308 | +def _compile_errors(info): |
| 309 | + """Create ``errors`` dict from ``info`` dict. """ |
| 310 | + errors_template = { |
| 311 | + 'tables': {}, |
| 312 | + 'tables_data': {}, |
| 313 | + } |
| 314 | + errors = deepcopy(errors_template) |
| 315 | + |
| 316 | + # first check if tables aren't a match |
| 317 | + if info['tables']['left_only']: |
| 318 | + errors['tables']['left_only'] = info['tables']['left_only'] |
| 319 | + |
| 320 | + if info['tables']['right_only']: |
| 321 | + errors['tables']['right_only'] = info['tables']['right_only'] |
| 322 | + |
| 323 | + # then check if there is a discrepancy in the data for each table |
| 324 | + keys = ['foreign_keys', 'primary_keys', 'indexes', 'columns'] |
| 325 | + subkeys = ['left_only', 'right_only', 'diff'] |
| 326 | + |
| 327 | + for table_name in info['tables_data']: |
| 328 | + for key in keys: |
| 329 | + for subkey in subkeys: |
| 330 | + if info['tables_data'][table_name][key][subkey]: |
| 331 | + table_d = errors['tables_data'].setdefault(table_name, {}) |
| 332 | + table_d.setdefault(key, {})[subkey] = info[ |
| 333 | + 'tables_data'][table_name][key][subkey] |
| 334 | + |
| 335 | + if errors != errors_template: |
| 336 | + errors['uris'] = info['uris'] |
| 337 | + return errors |
| 338 | + return {} |
| 339 | + |
| 340 | + |
| 341 | +def _make_result(info, errors): |
| 342 | + """Create a :class:`~.util.CompareResult` object. """ |
| 343 | + return CompareResult(info, errors) |
0 commit comments