| 6 |      1 | # -*- coding: utf-8 -*-
 | 
|  |      2 | #
 | 
|  |      3 | # PgDataDiff - compare tables, print data differencies
 | 
|  |      4 | #
 | 
|  |      5 | # Copyright (c) 2011  Radek Brich <radek.brich@devl.cz>
 | 
|  |      6 | #
 | 
|  |      7 | # Permission is hereby granted, free of charge, to any person obtaining a copy
 | 
|  |      8 | # of this software and associated documentation files (the "Software"), to deal
 | 
|  |      9 | # in the Software without restriction, including without limitation the rights
 | 
|  |     10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 | 
|  |     11 | # copies of the Software, and to permit persons to whom the Software is
 | 
|  |     12 | # furnished to do so, subject to the following conditions:
 | 
|  |     13 | #
 | 
|  |     14 | # The above copyright notice and this permission notice shall be included in
 | 
|  |     15 | # all copies or substantial portions of the Software.
 | 
|  |     16 | #
 | 
|  |     17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 | 
|  |     18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 | 
|  |     19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 | 
|  |     20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 | 
|  |     21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 | 
|  |     22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 | 
|  |     23 | # THE SOFTWARE.
 | 
|  |     24 | 
 | 
|  |     25 | 
 | 
|  |     26 | from psycopg2.extensions import adapt
 | 
|  |     27 | 
 | 
|  |     28 | from common.highlight import *
 | 
|  |     29 | from collections import OrderedDict
 | 
|  |     30 | 
 | 
|  |     31 | 
 | 
|  |     32 | class DiffData:
 | 
|  |     33 |     COLORS = {
 | 
|  |     34 |         '+' : BOLD | GREEN,
 | 
|  |     35 |         '-' : BOLD | RED,
 | 
|  |     36 |         '*' : BOLD | YELLOW,
 | 
|  |     37 |         'V' : BOLD | WHITE}
 | 
|  |     38 |     
 | 
|  |     39 |     def __init__(self, table, change, src_cols, dst_cols, id=None):
 | 
|  |     40 |         self.table = table
 | 
|  |     41 |         self.change = change
 | 
|  |     42 |         self.src_cols = src_cols
 | 
|  |     43 |         self.dst_cols = dst_cols
 | 
|  |     44 |         self.id = id
 | 
|  |     45 |     
 | 
|  |     46 |     def format(self):
 | 
|  |     47 |         out = []
 | 
|  |     48 |         
 | 
|  |     49 |         out.append(highlight(1, self.COLORS[self.change]))
 | 
|  |     50 |         out.extend([self.change, ' '])
 | 
|  |     51 |         
 | 
|  |     52 |         out.extend(self._format_changes())
 | 
|  |     53 |         
 | 
|  |     54 |         out.append(highlight(0))
 | 
|  |     55 |         
 | 
|  |     56 |         return ''.join(out)
 | 
|  |     57 | 
 | 
|  |     58 |     def format_patch(self):
 | 
|  |     59 |         method = {
 | 
|  |     60 |             '+' : self._format_insert,
 | 
|  |     61 |             '-' : self._format_delete,
 | 
|  |     62 |             '*' : self._format_update}
 | 
|  |     63 |         
 | 
|  |     64 |         return method[self.change]()
 | 
|  |     65 | 
 | 
|  |     66 |     def _format_changes(self):
 | 
|  |     67 |         if self.src_cols and not self.dst_cols:
 | 
|  |     68 |             return [', '.join([self._format_value_del(*x) for x in self.src_cols.items()])]
 | 
|  |     69 |         if not self.src_cols and self.dst_cols:
 | 
|  |     70 |             return [', '.join([self._format_value_add(*x) for x in self.dst_cols.items()])]
 | 
|  |     71 |         
 | 
|  |     72 |         items = []
 | 
|  |     73 |         for i in range(len(self.src_cols)):
 | 
|  |     74 |             items.append((
 | 
|  |     75 |                 list(self.src_cols.keys())[i],
 | 
|  |     76 |                 list(self.src_cols.values())[i],
 | 
|  |     77 |                 list(self.dst_cols.values())[i]))
 | 
|  |     78 |             
 | 
|  |     79 |         return [', '.join([self._format_value_change(*x) for x in items])]
 | 
|  |     80 | 
 | 
|  |     81 |     def _format_value_del(self, k, v):
 | 
|  |     82 |         fs = (highlight(1, self.COLORS['-']) + '{}: ' + highlight(0) + '{}')
 | 
|  |     83 |         return fs.format(k, adapt(v).getquoted().decode())
 | 
|  |     84 | 
 | 
|  |     85 |     def _format_value_add(self, k, v):
 | 
|  |     86 |         fs = (highlight(1, self.COLORS['+']) + '{}: ' + highlight(0) + 
 | 
|  |     87 |             highlight(1, self.COLORS['V']) + '{}' + highlight(0))
 | 
|  |     88 |         return fs.format(k, adapt(v).getquoted().decode())
 | 
|  |     89 | 
 | 
|  |     90 |     def _format_value_change(self, k, v1, v2):
 | 
|  |     91 |         fs = (highlight(1, self.COLORS['*']) + '{}: ' + highlight(0) + 
 | 
|  |     92 |             '{} â–¶ ' +
 | 
|  |     93 |             highlight(1, self.COLORS['V']) + '{}' + highlight(0))
 | 
|  |     94 |         return fs.format(k,
 | 
|  |     95 |             adapt(v1).getquoted().decode(),
 | 
|  |     96 |             adapt(v2).getquoted().decode())
 | 
|  |     97 | 
 | 
|  |     98 |     def _format_insert(self):
 | 
|  |     99 |         out = ['INSERT INTO ', self.table, ' (']
 | 
|  |    100 |         out.append(', '.join(self.dst_cols.keys()))
 | 
|  |    101 |         out.append(') VALUES (')
 | 
|  |    102 |         out.append(', '.join([adapt(v).getquoted().decode() for v in self.dst_cols.values()]))
 | 
|  |    103 |         out.append(');')
 | 
|  |    104 |         return ''.join(out)
 | 
|  |    105 |     
 | 
|  |    106 |     def _format_delete(self):
 | 
|  |    107 |         out = ['DELETE FROM ', self.table]
 | 
|  |    108 |         out.extend(self._format_where()) 
 | 
|  |    109 |         return ''.join(out)
 | 
|  |    110 |     
 | 
|  |    111 |     def _format_update(self):
 | 
|  |    112 |         out = ['UPDATE ', self.table, ' SET ']
 | 
|  |    113 |         out.append(', '.join([self._format_set(*x) for x in self.dst_cols.items()]))
 | 
|  |    114 |         out.extend(self._format_where())
 | 
|  |    115 |         return ''.join(out)
 | 
|  |    116 | 
 | 
|  |    117 |     def _format_set(self, k, v):
 | 
|  |    118 |         return '{} = {}'.format(k, adapt(v).getquoted().decode())
 | 
|  |    119 | 
 | 
|  |    120 |     def _format_where(self):
 | 
|  |    121 |         out = [' WHERE ']
 | 
|  |    122 |         out.extend([self.id[0], ' = '])
 | 
|  |    123 |         out.append(adapt(self.id[1]).getquoted().decode())
 | 
|  |    124 |         out.append(';')
 | 
|  |    125 |         return out
 | 
|  |    126 | 
 | 
|  |    127 | class PgDataDiff:
 | 
|  |    128 |     def __init__(self, table=None, src_rows=None, dst_rows=None, col_names=None):
 | 
|  |    129 |         self.allowcolor = False
 | 
|  |    130 |         self.table = table
 | 
|  |    131 |         self.src_rows = src_rows
 | 
|  |    132 |         self.dst_rows = dst_rows
 | 
|  |    133 |         self.col_names = col_names
 | 
|  |    134 |     
 | 
|  |    135 |     def iter_diff(self):
 | 
|  |    136 |         '''Return differencies between data of two tables.
 | 
|  |    137 |         
 | 
|  |    138 |         Yields one line at the time.
 | 
|  |    139 |         
 | 
|  |    140 |         '''
 | 
|  |    141 |         while True:
 | 
|  |    142 |             try:
 | 
|  |    143 |                 diff = self._compare_row(self.src_rows, self.dst_rows)
 | 
|  |    144 |             except IndexError:
 | 
|  |    145 |                 break
 | 
|  |    146 |             
 | 
|  |    147 |             if diff:
 | 
|  |    148 |                 yield diff
 | 
|  |    149 |         
 | 
|  |    150 |     def print_diff(self):
 | 
|  |    151 |         '''Print differencies between data of two tables.
 | 
|  |    152 |         
 | 
|  |    153 |         The output is in human readable form.
 | 
|  |    154 |         
 | 
|  |    155 |         Set allowcolor=True of PgDataDiff instance to get colored output.
 | 
|  |    156 |         
 | 
|  |    157 |         '''
 | 
|  |    158 |         for ln in self.iter_diff():
 | 
|  |    159 |             print(ln.format())
 | 
|  |    160 |     
 | 
|  |    161 |     def print_patch(self):
 | 
|  |    162 |         '''Print SQL script usable as patch for destination table.
 | 
|  |    163 |         
 | 
|  |    164 |         Supports INSERT, DELETE and UPDATE operations.
 | 
|  |    165 |         
 | 
|  |    166 |         '''
 | 
|  |    167 |         for ln in self.iter_diff():
 | 
|  |    168 |             print(ln.format_patch())
 | 
|  |    169 | 
 | 
|  |    170 |     def _compare_data(self, src, dst):
 | 
|  |    171 |         src_cols = OrderedDict()
 | 
|  |    172 |         dst_cols = OrderedDict()
 | 
|  |    173 |         for i in range(len(src)):
 | 
|  |    174 |             if src[i] != dst[i]:
 | 
|  |    175 |                 src_cols[self.col_names[i]] = src[i]
 | 
|  |    176 |                 dst_cols[self.col_names[i]] = dst[i]
 | 
|  |    177 |         if src_cols:
 | 
|  |    178 |             id = (self.col_names[0], src[0])
 | 
|  |    179 |             return DiffData(self.table, '*', src_cols, dst_cols, id=id)
 | 
|  |    180 |         
 | 
|  |    181 |         return None
 | 
|  |    182 |     
 | 
|  |    183 |     def _compare_row(self, src_rows, dst_rows):
 | 
|  |    184 |         if len(src_rows) and not len(dst_rows):
 | 
|  |    185 |             src = src_rows.pop(0)
 | 
|  |    186 |             src_cols = OrderedDict(zip(self.col_names, src))
 | 
|  |    187 |             return DiffData(self.table, '-', src_cols, None)
 | 
|  |    188 |         if not len(src_rows) and len(dst_rows):
 | 
|  |    189 |             dst = dst_rows.pop(0)
 | 
|  |    190 |             dst_cols = OrderedDict(zip(self.col_names, dst))
 | 
|  |    191 |             return DiffData(self.table, '+', None, dst_cols)
 | 
|  |    192 |         
 | 
|  |    193 |         src = src_rows[0]
 | 
|  |    194 |         dst = dst_rows[0]
 | 
|  |    195 |         
 | 
|  |    196 |         if src[0] < dst[0]:
 | 
|  |    197 |             del src_rows[0]
 | 
|  |    198 |             src_cols = OrderedDict(zip(self.col_names, src))
 | 
|  |    199 |             id = (self.col_names[0], src[0])
 | 
|  |    200 |             return DiffData(self.table, '-', src_cols, None, id=id)
 | 
|  |    201 |         if src[0] > dst[0]:
 | 
|  |    202 |             del dst_rows[0]
 | 
|  |    203 |             dst_cols = OrderedDict(zip(self.col_names, dst))
 | 
|  |    204 |             return DiffData(self.table, '+', None, dst_cols)
 | 
|  |    205 |         
 | 
|  |    206 |         del src_rows[0]
 | 
|  |    207 |         del dst_rows[0]
 | 
|  |    208 |         return self._compare_data(src, dst)
 | 
|  |    209 | 
 |