File Coverage

blib/lib/MySQL/Diff/Database.pm
Criterion Covered Total %
statement 24 154 15.5
branch 0 50 0.0
condition 0 6 0.0
subroutine 8 24 33.3
pod 7 8 87.5
total 39 242 16.1


line stmt bran cond sub pod time code
1             package MySQL::Diff::Database;
2              
3             =head1 NAME
4              
5             MySQL::Diff::Database - Database Definition Class
6              
7             =head1 SYNOPSIS
8              
9             use MySQL::Diff::Database;
10              
11             my $db = MySQL::Diff::Database->new(%options);
12             my $source = $db->source_type();
13             my $summary = $db->summary();
14             my $name = $db->name();
15             my @tables = $db->tables();
16             my $table_def = $db->table_by_name($table);
17              
18             my @dbs = MySQL::Diff::Database::available_dbs();
19              
20             =head1 DESCRIPTION
21              
22             Parses a database definition into component parts.
23              
24             =cut
25              
26 3     3   707 use warnings;
  3         4  
  3         375  
27 3     3   10 use strict;
  3         3  
  3         57  
28 3     3   1234 use String::ShellQuote qw(shell_quote);
  3         1969  
  3         258  
29              
30             our $VERSION = '0.50';
31              
32             # ------------------------------------------------------------------------------
33             # Libraries
34              
35 3     3   14 use Carp qw(:DEFAULT);
  3         3  
  3         396  
36 3     3   1404 use File::Slurp;
  3         30828  
  3         199  
37 3     3   1471 use IO::File;
  3         19660  
  3         323  
38              
39 3     3   1151 use MySQL::Diff::Utils qw(debug);
  3         3  
  3         130  
40 3     3   1007 use MySQL::Diff::Table;
  3         8  
  3         4248  
41              
42             # ------------------------------------------------------------------------------
43              
44             =head1 METHODS
45              
46             =head2 Constructor
47              
48             =over 4
49              
50             =item new( %options )
51              
52             Instantiate the objects, providing the command line options for database
53             access and process requirements.
54              
55             =back
56              
57             =cut
58              
59             sub new {
60 0     0 1   my $class = shift;
61 0           my %p = @_;
62 0           my $self = {};
63 0   0       bless $self, ref $class || $class;
64              
65 0           debug(3,"\nconstructing new MySQL::Diff::Database");
66              
67 0           my $auth_ref = _auth_args_string(%{$p{auth}});
  0            
68 0           my $string = shell_quote @$auth_ref;
69 0           debug(3,"auth args: $string");
70 0           $self->{_source}{auth} = $string;
71 0 0         $self->{_source}{dbh} = $p{dbh} if $p{dbh};
72 0           $self->{'single-transaction'} = $p{'single-transaction'};
73 0           $self->{'table-re'} = $p{'table-re'};
74              
75 0 0         if ($p{file}) {
    0          
76 0           $self->_canonicalise_file($p{file});
77             } elsif ($p{db}) {
78 0           $self->_read_db($p{db});
79             } else {
80 0           confess "MySQL::Diff::Database::new called without db or file params";
81             }
82              
83 0           $self->_parse_defs();
84 0           return $self;
85             }
86              
87             =head2 Public Methods
88              
89             =over 4
90              
91             =item * source_type()
92              
93             Returns 'file' if the data source is a text file, and 'db' if connected
94             directly to a database.
95              
96             =cut
97              
98             sub source_type {
99 0     0 1   my $self = shift;
100 0 0         return 'file' if $self->{_source}{file};
101 0 0         return 'db' if $self->{_source}{db};
102             }
103              
104             =item * summary()
105              
106             Provides a summary of the database.
107              
108             =cut
109              
110             sub summary {
111 0     0 1   my $self = shift;
112            
113 0 0         if ($self->{_source}{file}) {
    0          
114 0           return "file: " . $self->{_source}{file};
115             } elsif ($self->{_source}{db}) {
116 0           my $args = $self->{_source}{auth};
117 0           $args =~ tr/-//d;
118 0           $args =~ s/\bpassword=\S+//;
119 0           $args =~ s/^\s*(.*?)\s*$/$1/;
120 0           my $summary = " db: " . $self->{_source}{db};
121 0 0         $summary .= " ($args)" if $args;
122 0           return $summary;
123             } else {
124 0           return 'unknown';
125             }
126             }
127              
128             =item * name()
129              
130             Returns the name of the database.
131              
132             =cut
133              
134             sub name {
135 0     0 1   my $self = shift;
136 0   0       return $self->{_source}{file} || $self->{_source}{db};
137             }
138              
139             =item * tables()
140              
141             Returns a list of tables for the current database.
142              
143             =cut
144              
145             sub tables {
146 0     0 1   my $self = shift;
147 0           return @{$self->{_tables}};
  0            
148             }
149              
150             =item * table_by_name( $name )
151              
152             Returns the table definition (see L) for the given table.
153              
154             =cut
155              
156             sub table_by_name {
157 0     0 1   my ($self,$name) = @_;
158 0           return $self->{_by_name}{$name};
159             }
160              
161             =back
162              
163             =head1 FUNCTIONS
164              
165             =head2 Public Functions
166              
167             =over 4
168              
169             =item * available_dbs()
170              
171             Returns a list of the available databases.
172              
173             Note that is used as a function call, not a method call.
174              
175             =cut
176              
177             sub available_dbs {
178 0     0 1   my %auth = @_;
179 0           my $args_ref = _auth_args_string(%auth);
180 0           unshift @$args_ref, q{mysqlshow};
181            
182             # evil but we don't use DBI because I don't want to implement -p properly
183             # not that this works with -p anyway ...
184 0           my $command = shell_quote @$args_ref;
185 0 0         my $fh = IO::File->new("$command |") or die "Couldn't execute '$command': $!\n";
186 0           my $dbs_ref = _parse_mysqlshow_from_fh_into_arrayref($fh);
187 0 0         $fh->close() or die "$command failed: $!";
188              
189 0           return map { $_ => 1 } @{$dbs_ref};
  0            
  0            
190             }
191              
192             =back
193              
194             =cut
195              
196             # ------------------------------------------------------------------------------
197             # Private Methods
198              
199             sub auth_args {
200 0     0 0   return _auth_args_string();
201             }
202              
203             sub _canonicalise_file {
204 0     0     my ($self, $file) = @_;
205              
206 0           $self->{_source}{file} = $file;
207 0           debug(2,"fetching table defs from file $file");
208              
209             # FIXME: option to avoid create-and-dump bit
210             # create a temporary database using defs from file ...
211             # hopefully the temp db is unique!
212 0           my $temp_db = sprintf "test_mysqldiff-temp-%d_%d_%d", time(), $$, rand();
213 0           debug(3,"creating temporary database $temp_db");
214            
215 0           my $defs = read_file($file);
216 0 0         die "$file contains dangerous command '$1'; aborting.\n"
217             if $defs =~ /;\s*(use|((drop|create)\s+database))\b/i;
218            
219 0           my $args = $self->{_source}{auth};
220 0 0         my $fh = IO::File->new("| mysql $args") or die "Couldn't execute 'mysql$args': $!\n";
221 0           print $fh "\nCREATE DATABASE \`$temp_db\`;\nUSE \`$temp_db\`;\n";
222 0           print $fh $defs;
223 0           $fh->close;
224              
225             # ... and then retrieve defs from mysqldump. Hence we've used
226             # MySQL to massage the defs file into canonical form.
227 0           $self->_get_defs($temp_db);
228              
229 0           debug(3,"dropping temporary database $temp_db");
230 0 0         $fh = IO::File->new("| mysql $args") or die "Couldn't execute 'mysql$args': $!\n";
231 0           print $fh "DROP DATABASE \`$temp_db\`;\n";
232 0           $fh->close;
233             }
234              
235             sub _read_db {
236 0     0     my ($self, $db) = @_;
237 0           $self->{_source}{db} = $db;
238 0           debug(3, "fetching table defs from db $db");
239 0           $self->_get_defs($db);
240             }
241              
242             sub _get_tables_to_dump {
243 0     0     my ( $self, $db ) = @_;
244              
245 0           my $tables_ref = $self->_get_tables_in_db($db);
246              
247 0           my $compiled_table_re = qr/$self->{'table-re'}/;
248              
249 0           my @matching_tables = grep { $_ =~ $compiled_table_re } @{$tables_ref};
  0            
  0            
250              
251 0           return join( ' ', @matching_tables );
252             }
253              
254             sub _get_tables_in_db {
255 0     0     my ( $self, $db ) = @_;
256              
257 0           my $args = $self->{_source}{auth};
258              
259             # evil but we don't use DBI because I don't want to implement -p properly
260             # not that this works with -p anyway ...
261 0 0         my $fh = IO::File->new("mysqlshow $args $db|")
262             or die "Couldn't execute 'mysqlshow $args $db': $!\n";
263 0           my $tables_ref = _parse_mysqlshow_from_fh_into_arrayref($fh);
264 0 0         $fh->close() or die "mysqlshow $args $db failed: $!";
265              
266 0           return $tables_ref;
267             }
268              
269             # Note that is used as a function call, not a method call.
270             sub _parse_mysqlshow_from_fh_into_arrayref {
271 0     0     my ($fh) = @_;
272              
273 0           my @items;
274 0           while (<$fh>) {
275 0 0         next unless /^\| ([\w-]+)/;
276 0           push @items, $1;
277             }
278              
279 0           return \@items;
280             }
281              
282             sub _get_defs {
283 0     0     my ( $self, $db ) = @_;
284              
285 0           my $args = $self->{_source}{auth};
286 0 0         my $single_transaction = $self->{'single-transaction'} ? "--single-transaction" : "";
287 0           my $tables = ''; #dump all tables by default
288 0 0         if ( my $table_re = $self->{'table-re'} ) {
289 0           $tables = $self->_get_tables_to_dump($db);
290 0 0         if ( !length $tables ) { # No tables to dump
291 0           $self->{_defs} = [];
292 0           return;
293             }
294             }
295              
296 0 0         my $fh = IO::File->new("mysqldump -d $single_transaction $args $db $tables 2>&1 |")
297             or die "Couldn't read ${db}'s table defs via mysqldump: $!\n";
298              
299 0           debug( 3, "running mysqldump -d $single_transaction $args $db $tables" );
300 0           my $defs = $self->{_defs} = [<$fh>];
301 0           $fh->close;
302 0           my $exit_status = $? >> 8;
303              
304 0 0         if ( grep /mysqldump: Got error: .*: Unknown database/, @$defs ) {
    0          
305 0           die <
306             Failed to create temporary database $db
307             during canonicalization. Make sure that your mysql.db table has a row
308             authorizing full access to all databases matching 'test\\_%', and that
309             the database doesn't already exist.
310             EOF
311             } elsif ($exit_status) {
312             # If mysqldump exited with a non-zero status, then
313             # we can not reliably make a diff, so better to die and bubble that error up.
314 0           die "mysqldump failed. Exit status: $exit_status:\n" . join( "\n", @{$defs} );
  0            
315             }
316 0           return;
317             }
318              
319             sub _parse_defs {
320 0     0     my $self = shift;
321              
322 0 0         return if $self->{_tables};
323              
324 0           debug(2, "parsing table defs");
325 0           my $defs = join '', grep ! /^\s*(\#|--|SET|\/\*)/, @{$self->{_defs}};
  0            
326 0           $defs =~ s/`//sg;
327 0           my @tables = split /(?=^\s*(?:create|alter|drop)\s+table\s+)/im, $defs;
328 0           $self->{_tables} = [];
329 0           for my $table (@tables) {
330 0           debug(4, " table def [$table]");
331 0 0         if($table =~ /create\s+table/i) {
332 0           my $obj = MySQL::Diff::Table->new(source => $self->{_source}, def => $table);
333 0           push @{$self->{_tables}}, $obj;
  0            
334 0           $self->{_by_name}{$obj->name()} = $obj;
335             }
336             }
337             }
338              
339             sub _auth_args_string {
340 0     0     my %auth = @_;
341 0           my $args = [];
342 0           for my $arg (qw/host port user password socket/) {
343 0 0         push @$args, qq/--$arg=$auth{$arg}/ if $auth{$arg};
344             }
345 0           return $args;
346             }
347              
348             1;
349              
350             __END__