~percona-toolkit-dev/percona-toolkit/pt-agent

« back to all changes in this revision

Viewing changes to lib/RowDiff.pm

  • Committer: Daniel Nichter
  • Date: 2011-06-24 17:22:06 UTC
  • Revision ID: daniel@percona.com-20110624172206-c7q4s4ad6r260zz6
Add lib/, t/lib/, and sandbox/.  All modules are updated and passing on MySQL 5.1.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# This program is copyright 2011 Percona Inc.
 
2
# This program is copyright 2007-2009 Baron Schwartz.
 
3
# Feedback and improvements are welcome.
 
4
#
 
5
# THIS PROGRAM IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
 
6
# WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
 
7
# MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 
8
#
 
9
# This program is free software; you can redistribute it and/or modify it under
 
10
# the terms of the GNU General Public License as published by the Free Software
 
11
# Foundation, version 2; OR the Perl Artistic License.  On UNIX and similar
 
12
# systems, you can issue `man perlgpl' or `man perlartistic' to read these
 
13
# licenses.
 
14
#
 
15
# You should have received a copy of the GNU General Public License along with
 
16
# this program; if not, write to the Free Software Foundation, Inc., 59 Temple
 
17
# Place, Suite 330, Boston, MA  02111-1307  USA.
 
18
# ###########################################################################
 
19
# RowDiff package $Revision: 5697 $
 
20
# ###########################################################################
 
21
package RowDiff;
 
22
 
 
23
use strict;
 
24
use warnings FATAL => 'all';
 
25
use English qw(-no_match_vars);
 
26
 
 
27
use constant MKDEBUG => $ENV{MKDEBUG} || 0;
 
28
 
 
29
# Required args:
 
30
#   * dbh           obj: dbh used for collation-specific string comparisons
 
31
# Optional args:
 
32
#   * same_row      Callback when rows are identical
 
33
#   * not_in_left   Callback when right row is not in the left
 
34
#   * not_in_right  Callback when left row is not in the right
 
35
#   * key_cmp       Callback when a column value differs
 
36
#   * done          Callback that stops compare_sets() if it returns true
 
37
#   * trf           Callback to transform numeric values before comparison
 
38
sub new {
 
39
   my ( $class, %args ) = @_;
 
40
   die "I need a dbh" unless $args{dbh};
 
41
   my $self = { %args };
 
42
   return bless $self, $class;
 
43
}
 
44
 
 
45
# Arguments:
 
46
#   * left_sth    obj: sth
 
47
#   * right_sth   obj: sth
 
48
#   * syncer      obj: TableSync* module
 
49
#   * tbl_struct  hashref: table struct from TableParser::parser()
 
50
# Iterates through two sets of rows and finds differences.  Calls various
 
51
# methods on the $syncer object when it finds differences, passing these
 
52
# args and hashrefs to the differing rows ($lr and $rr).
 
53
sub compare_sets {
 
54
   my ( $self, %args ) = @_;
 
55
   my @required_args = qw(left_sth right_sth syncer tbl_struct);
 
56
   foreach my $arg ( @required_args ) {
 
57
      die "I need a $arg argument" unless defined $args{$arg};
 
58
   }
 
59
   my $left_sth   = $args{left_sth};
 
60
   my $right_sth  = $args{right_sth};
 
61
   my $syncer     = $args{syncer};
 
62
   my $tbl_struct = $args{tbl_struct};
 
63
 
 
64
   my ($lr, $rr);    # Current row from the left/right sths.
 
65
   $args{key_cols} = $syncer->key_cols();  # for key_cmp()
 
66
 
 
67
   # We have to manually track if the left or right sth is done
 
68
   # fetching rows because sth->{Active} is always true with
 
69
   # DBD::mysql v3. And we cannot simply while ( $lr || $rr )
 
70
   # because in the case where left and right have the same key,
 
71
   # we do this:
 
72
   #    $lr = $rr = undef; # Fetch another row from each side.
 
73
   # Unsetting both $lr and $rr there would cause while () to
 
74
   # terminate. (And while ( $lr && $rr ) is not what we want
 
75
   # either.) Furthermore, we need to avoid trying to fetch more
 
76
   # rows if there are none to fetch because doing this would
 
77
   # cause a DBI error ("fetch without execute"). That's why we
 
78
   # make these checks:
 
79
   #    if ( !$lr && !$left_done )
 
80
   #    if ( !$rr && !$right_done )
 
81
   # If you make changes here, be sure to test both RowDiff.t
 
82
   # and RowDiff-custom.t. Look inside the later to see what
 
83
   # is custom about it.
 
84
   my $left_done  = 0;
 
85
   my $right_done = 0;
 
86
   my $done       = $self->{done};
 
87
 
 
88
   do {
 
89
      if ( !$lr && !$left_done ) {
 
90
         MKDEBUG && _d('Fetching row from left');
 
91
         eval { $lr = $left_sth->fetchrow_hashref(); };
 
92
         MKDEBUG && $EVAL_ERROR && _d($EVAL_ERROR);
 
93
         $left_done = !$lr || $EVAL_ERROR ? 1 : 0;
 
94
      }
 
95
      elsif ( MKDEBUG ) {
 
96
         _d('Left still has rows');
 
97
      }
 
98
 
 
99
      if ( !$rr && !$right_done ) {
 
100
         MKDEBUG && _d('Fetching row from right');
 
101
         eval { $rr = $right_sth->fetchrow_hashref(); };
 
102
         MKDEBUG && $EVAL_ERROR && _d($EVAL_ERROR);
 
103
         $right_done = !$rr || $EVAL_ERROR ? 1 : 0;
 
104
      }
 
105
      elsif ( MKDEBUG ) {
 
106
         _d('Right still has rows');
 
107
      }
 
108
 
 
109
      my $cmp;
 
110
      if ( $lr && $rr ) {
 
111
         $cmp = $self->key_cmp(%args, lr => $lr, rr => $rr);
 
112
         MKDEBUG && _d('Key comparison on left and right:', $cmp);
 
113
      }
 
114
      if ( $lr || $rr ) {
 
115
         # If the current row is the "same row" on both sides, meaning the two
 
116
         # rows have the same key, check the contents of the row to see if
 
117
         # they're the same.
 
118
         if ( $lr && $rr && defined $cmp && $cmp == 0 ) {
 
119
            MKDEBUG && _d('Left and right have the same key');
 
120
            $syncer->same_row(%args, lr => $lr, rr => $rr);
 
121
            $self->{same_row}->(%args, lr => $lr, rr => $rr)
 
122
               if $self->{same_row};
 
123
            $lr = $rr = undef; # Fetch another row from each side.
 
124
         }
 
125
         # The row in the left doesn't exist in the right.
 
126
         elsif ( !$rr || ( defined $cmp && $cmp < 0 ) ) {
 
127
            MKDEBUG && _d('Left is not in right');
 
128
            $syncer->not_in_right(%args, lr => $lr, rr => $rr);
 
129
            $self->{not_in_right}->(%args, lr => $lr, rr => $rr)
 
130
               if $self->{not_in_right};
 
131
            $lr = undef;
 
132
         }
 
133
         # Symmetric to the above.
 
134
         else {
 
135
            MKDEBUG && _d('Right is not in left');
 
136
            $syncer->not_in_left(%args, lr => $lr, rr => $rr);
 
137
            $self->{not_in_left}->(%args, lr => $lr, rr => $rr)
 
138
               if $self->{not_in_left};
 
139
            $rr = undef;
 
140
         }
 
141
      }
 
142
      $left_done = $right_done = 1 if $done && $done->(%args);
 
143
   } while ( !($left_done && $right_done) );
 
144
   MKDEBUG && _d('No more rows');
 
145
   $syncer->done_with_rows();
 
146
}
 
147
 
 
148
# Compare two rows to determine how they should be ordered.  NULL sorts before
 
149
# defined values in MySQL, so I consider undef "less than." Numbers are easy to
 
150
# compare.  Otherwise string comparison is tricky.  This function must match
 
151
# MySQL exactly or the merge algorithm runs off the rails, so when in doubt I
 
152
# ask MySQL to compare strings for me.  I can handle numbers and "normal" latin1
 
153
# characters without asking MySQL.  See
 
154
# http://dev.mysql.com/doc/refman/5.0/en/charset-literal.html.  $r1 and $r2 are
 
155
# row hashrefs.  $key_cols is an arrayref of the key columns to compare.  $tbl is the
 
156
# structure returned by TableParser.  The result matches Perl's cmp or <=>
 
157
# operators:
 
158
# 1 cmp 0 =>  1
 
159
# 1 cmp 1 =>  0
 
160
# 1 cmp 2 => -1
 
161
# TODO: must generate the comparator function dynamically for speed, so we don't
 
162
# have to check the type of columns constantly
 
163
sub key_cmp {
 
164
   my ( $self, %args ) = @_;
 
165
   my @required_args = qw(lr rr key_cols tbl_struct);
 
166
   foreach my $arg ( @required_args ) {
 
167
      die "I need a $arg argument" unless exists $args{$arg};
 
168
   }
 
169
   my ($lr, $rr, $key_cols, $tbl_struct) = @args{@required_args};
 
170
   MKDEBUG && _d('Comparing keys using columns:', join(',', @$key_cols));
 
171
 
 
172
   # Optional callbacks.
 
173
   my $callback = $self->{key_cmp};
 
174
   my $trf      = $self->{trf};
 
175
 
 
176
   foreach my $col ( @$key_cols ) {
 
177
      my $l = $lr->{$col};
 
178
      my $r = $rr->{$col};
 
179
      if ( !defined $l || !defined $r ) {
 
180
         MKDEBUG && _d($col, 'is not defined in both rows');
 
181
         return defined $l ? 1 : defined $r ? -1 : 0;
 
182
      }
 
183
      else {
 
184
         if ( $tbl_struct->{is_numeric}->{$col} ) {   # Numeric column
 
185
            MKDEBUG && _d($col, 'is numeric');
 
186
            ($l, $r) = $trf->($l, $r, $tbl_struct, $col) if $trf;
 
187
            my $cmp = $l <=> $r;
 
188
            if ( $cmp ) {
 
189
               MKDEBUG && _d('Column', $col, 'differs:', $l, '!=', $r);
 
190
               $callback->($col, $l, $r) if $callback;
 
191
               return $cmp;
 
192
            }
 
193
         }
 
194
         # Do case-sensitive cmp, expecting most will be eq.  If that fails, try
 
195
         # a case-insensitive cmp if possible; otherwise ask MySQL how to sort.
 
196
         elsif ( $l ne $r ) {
 
197
            my $cmp;
 
198
            my $coll = $tbl_struct->{collation_for}->{$col};
 
199
            if ( $coll && ( $coll ne 'latin1_swedish_ci'
 
200
                           || $l =~ m/[^\040-\177]/ || $r =~ m/[^\040-\177]/) )
 
201
            {
 
202
               MKDEBUG && _d('Comparing', $col, 'via MySQL');
 
203
               $cmp = $self->db_cmp($coll, $l, $r);
 
204
            }
 
205
            else {
 
206
               MKDEBUG && _d('Comparing', $col, 'in lowercase');
 
207
               $cmp = lc $l cmp lc $r;
 
208
            }
 
209
            if ( $cmp ) {
 
210
               MKDEBUG && _d('Column', $col, 'differs:', $l, 'ne', $r);
 
211
               $callback->($col, $l, $r) if $callback;
 
212
               return $cmp;
 
213
            }
 
214
         }
 
215
      }
 
216
   }
 
217
   return 0;
 
218
}
 
219
 
 
220
sub db_cmp {
 
221
   my ( $self, $collation, $l, $r ) = @_;
 
222
   if ( !$self->{sth}->{$collation} ) {
 
223
      if ( !$self->{charset_for} ) {
 
224
         MKDEBUG && _d('Fetching collations from MySQL');
 
225
         my @collations = @{$self->{dbh}->selectall_arrayref(
 
226
            'SHOW COLLATION', {Slice => { collation => 1, charset => 1 }})};
 
227
         foreach my $collation ( @collations ) {
 
228
            $self->{charset_for}->{$collation->{collation}}
 
229
               = $collation->{charset};
 
230
         }
 
231
      }
 
232
      my $sql = "SELECT STRCMP(_$self->{charset_for}->{$collation}? COLLATE $collation, "
 
233
         . "_$self->{charset_for}->{$collation}? COLLATE $collation) AS res";
 
234
      MKDEBUG && _d($sql);
 
235
      $self->{sth}->{$collation} = $self->{dbh}->prepare($sql);
 
236
   }
 
237
   my $sth = $self->{sth}->{$collation};
 
238
   $sth->execute($l, $r);
 
239
   return $sth->fetchall_arrayref()->[0]->[0];
 
240
}
 
241
 
 
242
sub _d {
 
243
   my ($package, undef, $line) = caller 0;
 
244
   @_ = map { (my $temp = $_) =~ s/\n/\n# /g; $temp; }
 
245
        map { defined $_ ? $_ : 'undef' }
 
246
        @_;
 
247
   print STDERR "# $package:$line $PID ", join(' ', @_), "\n";
 
248
}
 
249
 
 
250
1;
 
251
 
 
252
# ###########################################################################
 
253
# End RowDiff package
 
254
# ###########################################################################