pgtoolkit/pgdatadiff.py
changeset 54 291473ab847c
parent 41 6aad5e35efe8
child 83 515fadd3d286
equal deleted inserted replaced
53:4a049a5af657 54:291473ab847c
    34         '+' : BOLD | GREEN,
    34         '+' : BOLD | GREEN,
    35         '-' : BOLD | RED,
    35         '-' : BOLD | RED,
    36         '*' : BOLD | YELLOW,
    36         '*' : BOLD | YELLOW,
    37         'V' : BOLD | WHITE,
    37         'V' : BOLD | WHITE,
    38         'K' : BOLD | BLUE}
    38         'K' : BOLD | BLUE}
    39     
    39 
    40     def __init__(self, change, cols1, cols2, key=None):
    40     def __init__(self, change, cols1, cols2, key=None):
    41         '''
    41         '''
    42         
    42 
    43         change - one of '+', '-', '*' (add, remove, update)
    43         change - one of '+', '-', '*' (add, remove, update)
    44         cols1 - original column values (OrderedDict)
    44         cols1 - original column values (OrderedDict)
    45         cols2 - new column values (OrderedDict)
    45         cols2 - new column values (OrderedDict)
    46         key - primary key columns (OrderedDict)
    46         key - primary key columns (OrderedDict)
    47         
    47 
    48         '''
    48         '''
    49         self.change = change
    49         self.change = change
    50         self.cols1 = cols1
    50         self.cols1 = cols1
    51         self.cols2 = cols2
    51         self.cols2 = cols2
    52         self.key = key
    52         self.key = key
    53     
    53 
    54     def format(self):
    54     def format(self):
    55         out = []
    55         out = []
    56                 
    56 
    57         out.append(highlight(1, self.COLORS[self.change]))
    57         out.append(highlight(1, self.COLORS[self.change]))
    58         out.extend([self.change, ' '])
    58         out.extend([self.change, ' '])
    59         
    59 
    60         out.extend(self._format_changes())
    60         out.extend(self._format_changes())
    61         
    61 
    62         out.append(highlight(0))
    62         out.append(highlight(0))
    63         
    63 
    64         return ''.join(out)
    64         return ''.join(out)
    65 
    65 
    66     def format_patch(self, table):
    66     def format_patch(self, table):
    67         method = {
    67         method = {
    68             '+' : self._format_insert,
    68             '+' : self._format_insert,
    69             '-' : self._format_delete,
    69             '-' : self._format_delete,
    70             '*' : self._format_update}
    70             '*' : self._format_update}
    71         
    71 
    72         return method[self.change](table)
    72         return method[self.change](table)
    73 
    73 
    74     def _format_changes(self):        
    74     def _format_changes(self):
    75         if self.cols1 and not self.cols2:
    75         if self.cols1 and not self.cols2:
    76             return [', '.join([self._format_value_del(*x) for x in self.cols1.items()])]
    76             return [', '.join([self._format_value_del(*x) for x in self.cols1.items()])]
    77         if not self.cols1 and self.cols2:
    77         if not self.cols1 and self.cols2:
    78             return [', '.join([self._format_value_add(*x) for x in self.cols2.items()])]
    78             return [', '.join([self._format_value_add(*x) for x in self.cols2.items()])]
    79         
    79 
    80         out = []        
    80         out = []
    81         if self.key:
    81         if self.key:
    82             for colname in self.key:
    82             for colname in self.key:
    83                 out.extend([highlight(1, self.COLORS['*']), colname, ': ', highlight(0), self.key[colname], ', '])
    83                 out.extend([highlight(1, self.COLORS['*']), colname, ': ', highlight(0), self.key[colname], ', '])
    84 
    84 
    85         items = []
    85         items = []
    87             items.append((
    87             items.append((
    88                 list(self.cols1.keys())[i],
    88                 list(self.cols1.keys())[i],
    89                 list(self.cols1.values())[i],
    89                 list(self.cols1.values())[i],
    90                 list(self.cols2.values())[i]))
    90                 list(self.cols2.values())[i]))
    91         out.extend([', '.join([self._format_value_change(*x) for x in items])])
    91         out.extend([', '.join([self._format_value_change(*x) for x in items])])
    92         
    92 
    93         return out
    93         return out
    94 
    94 
    95     def _format_value_del(self, k, v):
    95     def _format_value_del(self, k, v):
    96         fs = (highlight(1, self.COLORS['-']) + '{}: ' + highlight(0) + '{}')
    96         fs = (highlight(1, self.COLORS['-']) + '{}: ' + highlight(0) + '{}')
    97         return fs.format(k, v)
    97         return fs.format(k, v)
    98 
    98 
    99     def _format_value_add(self, k, v):
    99     def _format_value_add(self, k, v):
   100         fs = (highlight(1, self.COLORS['+']) + '{}: ' + highlight(0) + 
   100         fs = (highlight(1, self.COLORS['+']) + '{}: ' + highlight(0) +
   101             highlight(1, self.COLORS['V']) + '{}' + highlight(0))
   101             highlight(1, self.COLORS['V']) + '{}' + highlight(0))
   102         return fs.format(k, v)
   102         return fs.format(k, v)
   103 
   103 
   104     def _format_value_change(self, k, v1, v2):
   104     def _format_value_change(self, k, v1, v2):
   105         fs = (highlight(1, self.COLORS['*']) + '{}: ' + highlight(0) + 
   105         fs = (highlight(1, self.COLORS['*']) + '{}: ' + highlight(0) +
   106             '{} ▶ ' +
   106             '{} ▶ ' +
   107             highlight(1, self.COLORS['V']) + '{}' + highlight(0))
   107             highlight(1, self.COLORS['V']) + '{}' + highlight(0))
   108         return fs.format(k, v1, v2)
   108         return fs.format(k, v1, v2)
   109 
   109 
   110     def _format_insert(self, table):
   110     def _format_insert(self, table):
   112         out.append(', '.join(self.cols2.keys()))
   112         out.append(', '.join(self.cols2.keys()))
   113         out.append(') VALUES (')
   113         out.append(') VALUES (')
   114         out.append(', '.join(self.cols2.values()))
   114         out.append(', '.join(self.cols2.values()))
   115         out.append(');')
   115         out.append(');')
   116         return ''.join(out)
   116         return ''.join(out)
   117     
   117 
   118     def _format_delete(self, table):
   118     def _format_delete(self, table):
   119         out = ['DELETE FROM ', table]
   119         out = ['DELETE FROM ', table]
   120         out.extend(self._format_where()) 
   120         out.extend(self._format_where())
   121         return ''.join(out)
   121         return ''.join(out)
   122     
   122 
   123     def _format_update(self, table):
   123     def _format_update(self, table):
   124         out = ['UPDATE ', table, ' SET ']
   124         out = ['UPDATE ', table, ' SET ']
   125         out.append(', '.join([self._format_set(*x) for x in self.cols2.items()]))
   125         out.append(', '.join([self._format_set(*x) for x in self.cols2.items()]))
   126         out.extend(self._format_where())
   126         out.extend(self._format_where())
   127         return ''.join(out)
   127         return ''.join(out)
   141         self.allowcolor = False
   141         self.allowcolor = False
   142         self.conn1 = conn1
   142         self.conn1 = conn1
   143         self.conn2 = conn2
   143         self.conn2 = conn2
   144         self.fulltable1 = None
   144         self.fulltable1 = None
   145         self.fulltable2 = None
   145         self.fulltable2 = None
   146     
   146 
   147     def settable1(self, table, schema='public'):
   147     def settable1(self, table, schema='public'):
   148         self.schema1 = schema
   148         self.schema1 = schema
   149         self.table1 = table
   149         self.table1 = table
   150         self.fulltable1 = '"' + schema + '"."'+ table + '"'
   150         self.fulltable1 = '"' + schema + '"."'+ table + '"'
   151         
   151 
   152     def settable2(self, table, schema='public'):
   152     def settable2(self, table, schema='public'):
   153         self.schema2 = schema
   153         self.schema2 = schema
   154         self.table2 = table
   154         self.table2 = table
   155         self.fulltable2 = '"' + schema + '"."'+ table + '"'
   155         self.fulltable2 = '"' + schema + '"."'+ table + '"'
   156     
   156 
   157     def iter_diff(self):
   157     def iter_diff(self):
   158         '''Return differencies between data of two tables.
   158         '''Return differencies between data of two tables.
   159         
   159 
   160         Yields one line at the time.
   160         Yields one line at the time.
   161         
   161 
   162         '''
   162         '''
   163         curs1, curs2 = self._select()
   163         curs1, curs2 = self._select()
   164         
   164 
   165         row1 = curs1.fetchone_dict()
   165         row1 = curs1.fetchone_dict()
   166         row2 = curs2.fetchone_dict()
   166         row2 = curs2.fetchone_dict()
   167         
   167 
   168         while True:
   168         while True:
   169             if row1 is None and row2 is None:
   169             if row1 is None and row2 is None:
   170                 break
   170                 break
   171             diff = self._compare_row(row1, row2, curs1.adapt, curs2.adapt)
   171             diff = self._compare_row(row1, row2, curs1.adapt, curs2.adapt)
   172             
   172 
   173             if diff:
   173             if diff:
   174                 yield diff
   174                 yield diff
   175             
   175 
   176                 if diff.change == '-':
   176                 if diff.change == '-':
   177                     row1 = curs1.fetchone_dict()
   177                     row1 = curs1.fetchone_dict()
   178                     continue
   178                     continue
   179                 if diff.change == '+':
   179                 if diff.change == '+':
   180                     row2 = curs2.fetchone_dict()
   180                     row2 = curs2.fetchone_dict()
   181                     continue
   181                     continue
   182             # change == '*' or not diff
   182             # change == '*' or not diff
   183             row1 = curs1.fetchone_dict()
   183             row1 = curs1.fetchone_dict()
   184             row2 = curs2.fetchone_dict()
   184             row2 = curs2.fetchone_dict()
   185         
   185 
   186         curs1.close()
   186         curs1.close()
   187         curs2.close()
   187         curs2.close()
   188     
   188 
   189     def print_diff(self):
   189     def print_diff(self):
   190         '''Print differencies between data of two tables.
   190         '''Print differencies between data of two tables.
   191         
   191 
   192         The output is in human readable form.
   192         The output is in human readable form.
   193         
   193 
   194         Set allowcolor=True of PgDataDiff instance to get colored output.
   194         Set allowcolor=True of PgDataDiff instance to get colored output.
   195         
   195 
   196         '''
   196         '''
   197         for ln in self.iter_diff():
   197         for ln in self.iter_diff():
   198             print(ln.format())
   198             print(ln.format())
   199     
   199 
   200     def print_patch(self):
   200     def print_patch(self):
   201         '''Print SQL script usable as patch for destination table.
   201         '''Print SQL script usable as patch for destination table.
   202         
   202 
   203         Supports INSERT, DELETE and UPDATE operations.
   203         Supports INSERT, DELETE and UPDATE operations.
   204         
   204 
   205         '''
   205         '''
   206         for ln in self.iter_diff():
   206         for ln in self.iter_diff():
   207             print(ln.format_patch(self.fulltable1))
   207             print(ln.format_patch(self.fulltable1))
   208 
   208 
   209     def _select(self):
   209     def _select(self):
   210         browser = pgbrowser.PgBrowser(self.conn1)
   210         browser = pgbrowser.PgBrowser(self.conn1)
   211         
   211 
   212         columns = browser.list_columns(schema=self.schema1, table=self.table1, order=1)
   212         columns = browser.list_columns(schema=self.schema1, table=self.table1, order=1)
   213         if not columns:
   213         if not columns:
   214             raise Exception('Table %s.%s not found.' % (self.schema1, self.table1))
   214             raise Exception('Table %s.%s not found.' % (self.schema1, self.table1))
   215         columns_sel = ', '.join(['"' + x['name'] + '"' for x in columns])
   215         columns_sel = ', '.join(['"' + x['name'] + '"' for x in columns])
   216         self.colnames = [x['name'] for x in columns]
   216         self.colnames = [x['name'] for x in columns]
   217         
   217 
   218         pkey = [ind for ind in browser.list_indexes(schema=self.schema1, table=self.table1) if ind['primary']]
   218         pkey = [ind for ind in browser.list_indexes(schema=self.schema1, table=self.table1) if ind['primary']]
   219         if not pkey:
   219         if not pkey:
   220             raise Exception('Table %s.%s has no primary key.' % (self.schema1, self.table1))
   220             raise Exception('Table %s.%s has no primary key.' % (self.schema1, self.table1))
   221         pkey = pkey[0]
   221         pkey = pkey[0]
   222         pkey_sel = ', '.join(['"' + x + '"' for x in pkey['columns']])
   222         pkey_sel = ', '.join(['"' + x + '"' for x in pkey['columns']])
   223         self.pkeycolnames = pkey['columns']
   223         self.pkeycolnames = pkey['columns']
   224         
   224 
   225         query1 = 'SELECT ' + columns_sel + ' FROM ' + self.fulltable1 + ' ORDER BY ' + pkey_sel
   225         query1 = 'SELECT ' + columns_sel + ' FROM ' + self.fulltable1 + ' ORDER BY ' + pkey_sel
   226         query2 = 'SELECT ' + columns_sel + ' FROM ' + self.fulltable2 + ' ORDER BY ' + pkey_sel
   226         query2 = 'SELECT ' + columns_sel + ' FROM ' + self.fulltable2 + ' ORDER BY ' + pkey_sel
   227         
   227 
   228         curs1 = self.conn1.cursor('curs1')
   228         curs1 = self.conn1.cursor('curs1')
   229         curs2 = self.conn2.cursor('curs2')
   229         curs2 = self.conn2.cursor('curs2')
   230         
   230 
   231         curs1.execute(query1)
   231         curs1.execute(query1)
   232         curs2.execute(query2)
   232         curs2.execute(query2)
   233         
   233 
   234         return curs1, curs2
   234         return curs1, curs2
   235 
   235 
   236     def _compare_data(self, row1, row2):
   236     def _compare_data(self, row1, row2):
   237         cols1 = OrderedDict()
   237         cols1 = OrderedDict()
   238         cols2 = OrderedDict()
   238         cols2 = OrderedDict()
   239         for name in row1:
   239         for name in row1.keys():
   240             if row1[name] != row2[name]:
   240             if row1[name] != row2[name]:
   241                 cols1[name] = row1[name]
   241                 cols1[name] = row1[name]
   242                 cols2[name] = row2[name]
   242                 cols2[name] = row2[name]
   243         if cols1:
   243         if cols1:
   244             key = OrderedDict(zip(self.pkeycolnames, [row1[colname] for colname in self.pkeycolnames]))
   244             key = OrderedDict(zip(self.pkeycolnames, [row1[colname] for colname in self.pkeycolnames]))
   245             return DiffData('*', cols1, cols2, key=key)
   245             return DiffData('*', cols1, cols2, key=key)
   246         
   246 
   247         return None
   247         return None
   248     
   248 
   249     def _compare_row(self, row1, row2, adapt1, adapt2):
   249     def _compare_row(self, row1, row2, adapt1, adapt2):
   250         if row2 is None:
   250         if row2 is None:
   251             row1 = adapt1(row1)
   251             row1 = adapt1(row1)
   252             key = OrderedDict(zip(self.pkeycolnames, [row1[colname] for colname in self.pkeycolnames]))
   252             key = OrderedDict(zip(self.pkeycolnames, [row1[colname] for colname in self.pkeycolnames]))
   253             return DiffData('-', row1, None, key=key)
   253             return DiffData('-', row1, None, key=key)
   254         if row1 is None:
   254         if row1 is None:
   255             row2 = adapt2(row2)
   255             row2 = adapt2(row2)
   256             return DiffData('+', None, row2)
   256             return DiffData('+', None, row2)
   257         
   257 
   258         for keyname in self.pkeycolnames:
   258         for keyname in self.pkeycolnames:
   259             if row1[keyname] < row2[keyname]:
   259             if row1[keyname] < row2[keyname]:
   260                 row1 = adapt1(row1)
   260                 row1 = adapt1(row1)
   261                 key = OrderedDict(zip(self.pkeycolnames, [row1[colname] for colname in self.pkeycolnames]))
   261                 key = OrderedDict(zip(self.pkeycolnames, [row1[colname] for colname in self.pkeycolnames]))
   262                 return DiffData('-', row1, None, key=key)
   262                 return DiffData('-', row1, None, key=key)
   263         for keyname in self.pkeycolnames:
   263         for keyname in self.pkeycolnames:
   264             if row1[keyname] > row2[keyname]:
   264             if row1[keyname] > row2[keyname]:
   265                 row2 = adapt2(row2)
   265                 row2 = adapt2(row2)
   266                 return DiffData('+', None, row2)
   266                 return DiffData('+', None, row2)
   267         
   267 
   268         row1 = adapt1(row1)
   268         row1 = adapt1(row1)
   269         row2 = adapt2(row2)
   269         row2 = adapt2(row2)
   270         return self._compare_data(row1, row2)
   270         return self._compare_data(row1, row2)
   271 
   271