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): |
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 |