#!/usr/bin/perl

eval 'exec /usr/bin/perl  -S $0 ${1+"$@"}'
    if 0; # not running under some shell

# This program sorts and filters table checksums output by mysql-table-checksum,
# and only shows you ones that have problems.
#
# This program is copyright (c) 2007 Baron Schwartz.
# Feedback and improvements are welcome.
#
# THIS PROGRAM IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
# WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
# MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation, version 2; OR the Perl Artistic License.  On UNIX and similar
# systems, you can issue `man perlgpl' or `man perlartistic' to read these
# licenses.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc., 59 Temple
# Place, Suite 330, Boston, MA  02111-1307  USA.

use strict;
use warnings FATAL => 'all';

use English qw(-no_match_vars);
use Getopt::Long;
use List::Util qw(max);

our $VERSION = '1.1.14';
our $DISTRIB = '848';
our $SVN_REV = sprintf("%d", q$Revision: 655 $ =~ m/(\d+)/g);

$OUTPUT_AUTOFLUSH = 1;

# ############################################################################
# Get configuration information.
# ############################################################################

# Define cmdline args; each is GetOpt::Long spec, whether required,
# human-readable description.  Add more hash entries as needed.
my @opt_spec = (
   { s => 'header|h',   d => 'Keep the headers' },
   { s => 'help',       d => 'Show this help message' },
   { s => 'master|m=s', d => 'Name of the master server' },
   { s => 'verbose|v',  d => 'Output lines that have no differences' },
   { s => 'version',    d => 'Output version information and exit' },
);
# This is the container for the command-line options' values to be stored in
# after processing.  Initial values are defaults.
my %opts = ( m => '' );
# Post-process...
my %opt_seen;
foreach my $spec ( @opt_spec ) {
   my ( $long, $short ) = $spec->{s} =~ m/^(\w+)(?:\|([^!+=]*))?/;
   $spec->{k} = $short || $long;
   $spec->{l} = $long;
   $spec->{t} = $short;
   $spec->{n} = $spec->{s} =~ m/!/;
   $opts{$spec->{k}} = undef unless defined $opts{$spec->{k}};
   die "Duplicate option $spec->{k}" if $opt_seen{$spec->{k}}++;
}

Getopt::Long::Configure('no_ignore_case', 'bundling');
GetOptions( map { $_->{s} => \$opts{$_->{k}} } @opt_spec) or $opts{help} = 1;

if ( $opts{version} ) {
   print "mysql-checksum-filter  Ver $VERSION Distrib $DISTRIB Changeset $SVN_REV\n";
   exit(0);
}

if ( $opts{help} ) {
   print "Usage: mysql-checksum-filter <options> FILE\n\n";
   my $maxw = max(map { length($_->{l}) + ($_->{n} ? 4 : 0)} @opt_spec);
   foreach my $spec ( sort { $a->{l} cmp $b->{l} } @opt_spec ) {
      my $long  = $spec->{n} ? "[no]$spec->{l}" : $spec->{l};
      my $short = $spec->{t} ? "-$spec->{t}" : '';
      printf("  --%-${maxw}s %-4s %s\n", $long, $short, $spec->{d});
   }
   print <<USAGE;

Filter checksums from mysql-table-checksum and print those that differ.  With
no FILE, or when FILE is -, read standard input.  For more details, please
read the documentation:

   perldoc mysql-checksum-filter

USAGE
   exit(0);
}

# ############################################################################
# Ready to work now.
# ############################################################################

my $exit_status = 0;
my $one_file    = @ARGV < 2;

my $DATABASE = 0;
my $TABLE    = 1;
my $CHUNK    = 2;
my $HOST     = 3;
my $ENGINE   = 4;
my $COUNT    = 5;
my $CHECKSUM = 6;
my $TIME     = 7;
my $WAIT     = 8;
my $STAT     = 9;
my $LAG      = 10;

my $ORIG     = 11; # Added during processing, not part of input.

my $num_cols = 11;

my %lines_for;    # All lines from all files, if > 1 file on cmdline
my $last_chunk;     # db.tbl.chunk of last line read
my $current_set = [];    # working set of lines to process

LINE:
while ( my $line = <> ) {    # Magically reads STDIN or files in @ARGV
   chomp $line;
   next unless $line;

   if ( $one_file && $line =~ m/^DATABASE/ && $opts{h} ) {
      print $line, "\n";
      next LINE;
   }

   my @cols = $line =~ m/(\S+)/g;
   next unless @cols == $num_cols;
   push @cols, $line;
   my $chunk = join('.', @cols[$DATABASE, $TABLE, $CHUNK]);

   if ($one_file) { # Process immediately.
      if ( $last_chunk && $last_chunk ne $chunk ) {
         process_set($current_set);
         $current_set = [ \@cols ];
      }
      else {
         push @$current_set, \@cols;
      }
   }
   else { # Stash into %lines_for and process later.
      $lines_for{$chunk} ||= [];
      push @{ $lines_for{$chunk} }, \@cols;
   }
   $last_chunk = $chunk;
}

if ($one_file) {
   process_set($current_set);
}
else {
   foreach my $set ( values %lines_for ) {
      process_set($set);
   }
}

sub process_set {
   my ($set) = @_;
   return unless @$set;

   # Sort the "master" to the front, all others in aphabetical order
   @$set = sort {
           $a->[$HOST] eq $opts{m} ? -1
         : $b->[$HOST] eq $opts{m} ? 1
         : $a->[$HOST] cmp $b->[$HOST]
   } @$set;

   # If the verbose flag is set, or if anything differs, print it
   my $first = $set->[0];

   my $print = $opts{v} || grep {
            $_->[$CHECKSUM] ne $first->[$CHECKSUM]
         || $_->[$COUNT]    ne $first->[$COUNT]
   } @{$set}[ 1 .. ( scalar(@$set) - 1 ) ];

   if ($print) {
      foreach my $line (@$set) {
         print $line->[$ORIG], "\n";
      }
   }
}

exit $exit_status;

# ############################################################################
# Documentation
# ############################################################################
=pod

=head1 NAME

mysql-checksum-filter - Filter checksums from mysql-table-checksum.

=head1 SYNOPSIS

   mysql-checksum-filter checksums.txt
   mysql-table-checksum host1 host2 | mysql-checksum-filter

=head1 OVERVIEW

This program takes the unsorted, verbose output from L<mysql-table-checksum> and
sorts it, then filters it so you only see lines that have different checksums
or counts.

You can pipe input directly into it from L<mysql-table-checksum>, or you can
save the mysql-table-checksum's output and run mysql-checksum-filter on the
resulting file(s).  If you run it against just one file, or pipe output
directly into it, it'll output results during processing.  Processing multiple
files is slightly more expensive, and you won't see any output until they're
all read.

=head1 OPTIONS

=over

=item --header

Preserves headers output by MySQL Table Checksum.

=item --help

Displays a help message.

=item --master

Specifies which host is the replication master, and sorts lines for that host
first, so you can see the checksum values on the master server before the
slave.

=item --verbose

Output all lines except header lines.

=item --version

Output version information and exit.

=back

=head1 BUGS

Please use the Sourceforge bug tracker, forums, and mailing lists to request
support or report bugs: L<http://sourceforge.net/projects/mysqltoolkit/>.

=head1 AUTHOR

Baron "Xaprb" Schwartz.

=head1 COPYRIGHT, LICENSE AND WARRANTY

This program is copyright (c) 2007 Baron Schwartz.
Feedback and improvements are welcome.

THIS PROGRAM IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.

This program is free software; you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free Software
Foundation, version 2; OR the Perl Artistic License.  On UNIX and similar
systems, you can issue `man perlgpl' or `man perlartistic' to read these
licenses.

You should have received a copy of the GNU General Public License along with
this program; if not, write to the Free Software Foundation, Inc., 59 Temple
Place, Suite 330, Boston, MA  02111-1307  USA.

=head1 VERSION

This manual page documents Ver 1.1.14 Distrib 848 $Revision $.

=cut
