File Coverage

lib/DBIx/MyDatabaseMunger.pm
Criterion Covered Total %
statement 21 765 2.7
branch 0 308 0.0
condition 0 57 0.0
subroutine 7 74 9.4
pod 57 62 91.9
total 85 1266 6.7


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             DBIx::MyDatabaseMunger - MariaDB/MySQL Database Management Utility
4              
5             =head1 SYNOPSIS
6              
7             Normal interface is through the mydbmunger command, but this class can also
8             be used directly.
9              
10             use DBIx::MyDatabaseMunger ();
11             $dbmunger = new DBIx::MyDatabaseMunger ({
12             connect => {
13             schema => 'database',
14             host => 'mysql.example.com',
15             user => 'username',
16             password => 'p4ssw0rd',
17             },
18             colname => {
19             ctime => 'create_datetime',
20             mtime => 'tstmp',
21             },
22             });
23             $dbmunger->pull();
24             $dbmunger->make_archive();
25             $dbmunger->push();
26              
27             =head1 DESCRIPTION
28              
29             A library and accompanying "mydbmunger" utility to simplify complex MySQL and
30             MariaDB database management tasks.
31              
32             =cut
33              
34             package DBIx::MyDatabaseMunger;
35 12     12   5651 use strict;
  12         14  
  12         257  
36 12     12   32 use warnings;
  12         12  
  12         214  
37 12     12   4189 use autodie;
  12         128874  
  12         52  
38 12     12   49856 use Storable qw(dclone freeze);
  12         26607  
  12         639  
39 12     12   4501 use SQL::QueryBuilder::Pretty ();
  12         171506  
  12         800  
40              
41             our $VERSION = 0.80;
42             our $DRYRUN = 0;
43             our $QUIET = 0;
44             our $VERBOSE = 0;
45             our $SQL_PRETTY = SQL::QueryBuilder::Pretty->new(
46             '-database' => 'MySQL',
47             );
48              
49             # When running the todo list, do these things in this order.
50 12         2271 use constant TODO_ACTIONS => qw(
51             drop_constraint
52             drop_trigger
53             drop_procedure
54             drop_view
55             drop_key
56             drop_column
57             drop_table
58             create_table
59             add_column
60             modify_column
61             add_key
62             add_constraint
63             create_view
64             create_procedure
65             create_trigger
66 12     12   56 );
  12         17  
67              
68             =head1 CONSTRUCTOR
69              
70             The constructor C takes a hash reference of
71             options. These options include.
72              
73             =over 4
74              
75             =item C
76              
77             Naming convention for archive tables. Takes a wildcard, '%' that will be the
78             source table name.
79              
80             Default: C<%Archive>
81              
82             =item C
83              
84             A hash of column names for special handling. Column names include:
85              
86             =over 4
87              
88             =item C
89              
90             Column used to record action in archive table. Column should be an enumeration
91             type with values 'insert', 'update', or 'delete'.
92              
93             =item C
94              
95             Column used to record when a record is initially created. If not specified
96             then this functionality will not be implemented.
97              
98             =item C
99              
100             Column used to track dabase connection C, which indicates the user and
101             host that is connected to the database.
102              
103             =item C
104              
105             Column used to record when a record was last changed. If not specified then
106             this functionality will not be implemented.
107              
108             =item C
109              
110             Revision count column. Must be an integer type.
111              
112             =item C
113              
114             The column used to track the SQL statement responsible for a table change.
115              
116             =item C
117              
118             The column used to store the value of the variable indicated by C.
119              
120             =back
121              
122             =item C
123              
124             Directory in which to save table and trigger definitions.
125              
126             Default: C<.>
127              
128             =item C
129              
130             Connection variable to be used by the calling application to track the reason
131             for table updates, inserts, and deletes.
132              
133             Default: C<@updid>
134              
135             =back
136              
137             =cut
138              
139             sub new
140             {
141 0     0 0   my $class = shift;
142              
143 0           my $self = dclone( $_[0] );
144              
145             # Apply default values.
146 0   0       $self->{dir} ||= '.';
147 0   0       $self->{archive_name_pattern} ||= '%Archive';
148 0   0       $self->{updidvar} ||= '@updid';
149 0   0       $self->{colname}{action} ||= 'action';
150 0   0       $self->{colname}{dbuser} ||= 'dbuser';
151 0   0       $self->{colname}{revision} ||= 'revision';
152 0   0       $self->{colname}{stmt} ||= 'stmt';
153 0   0       $self->{colname}{updid} ||= 'updid';
154              
155             # Initialize todo queues.
156 0           $self->{todo} = { map {($_=>[])} TODO_ACTIONS };
  0            
157              
158 0           return bless $self, $class;
159             }
160              
161             =head1 METHODS
162              
163             =cut
164              
165             #
166             # PRIVATE UTILITY FUNCTIONS
167             #
168              
169             ### $self->__dbi_connect ()
170             #
171             # Connect to database, set $self->{dbh}.
172             #
173             sub __dbi_connect : method
174             {
175 0     0     my $self = shift;
176 12     12   15381 use DBI ();
  12         139101  
  12         101233  
177              
178             die "No database schema specified.\n"
179 0 0         unless $self->{connect}{schema};
180              
181             # Build Perl DBI dsn
182 0           my $dsn = "DBI:mysql:database=$self->{connect}{schema}";
183 0 0         $dsn .= ";host=$self->{connect}{host}" if $self->{connect}{host};
184 0 0         $dsn .= ";port=$self->{connect}{port}" if $self->{connect}{port};
185              
186             my $dbh = DBI->connect(
187             $dsn,
188             $self->{connect}{user},
189             $self->{connect}{password},
190 0           { PrintError => 0, RaiseError => 1 }
191             );
192 0           $self->{dbh} = $dbh;
193             }
194              
195             ### $self->__ignore_table ( $name )
196             #
197             # Determine whether a table should be ignored based on the tables setting.
198             #
199             sub __ignore_table : method
200             {
201 0     0     my $self = shift;
202 0           my($name) = @_;
203              
204              
205             # Skip table if explicitly excluded.
206 0 0         if( $self->{exclude_tables} ) {
207 0           for my $t ( @{ $self->{exclude_tables} } ) {
  0            
208 0 0         return 1 if $name eq $t;
209              
210             # On to the next table unless this one is a wildcard.
211 0 0         next unless $t =~ m/%/;
212              
213             # Build regex by splitting table specification on '%' and replacing
214             # it with '.*'. Make sure interemediate chunks of the specification
215             # are regex quoted with qr using \Q...\E. Then add beginning and
216             # end of string anchors, '^' and '$'.
217 0           my $re = '^'.join('.*', map { qr/\Q$_\E/ } split '%', $t, -1 ).'$';
  0            
218              
219             # Ignore table if the regex matches.
220 0 0         return 1 if $name =~ $re;
221             }
222             }
223              
224             # Don't skip any more tables if tables list is empty.
225 0 0         return 0 unless @{ $self->{tables} };
  0            
226              
227             # Skip table if not listed expliitly in tables.
228 0           for my $t ( @{ $self->{tables} } ) {
  0            
229 0 0         return 0 if $name eq $t;
230              
231             # On to the next table unless this one is a wildcard.
232 0 0         next unless $t =~ m/%/;
233              
234             # Build regex by splitting table specification on '%' and replacing
235             # it with '.*'. Make sure interemediate chunks of the specification
236             # are regex quoted with qr using \Q...\E. Then add beginning and end
237             # of string anchors, '^' and '$'.
238 0           my $re = '^'.join('.*', map { qr/\Q$_\E/ } split '%', $t, -1 ).'$';
  0            
239              
240             # Don't ignore table if the regex matches.
241 0 0         return 0 if $name =~ $re;
242             }
243 0           return 1;
244             }
245              
246             ### $self->__ignore_view ( $name )
247             #
248             # Determine whether a view should be ignored based on the views setting.
249             #
250             sub __ignore_view : method
251             {
252 0     0     my $self = shift;
253 0           my($name) = @_;
254              
255              
256             # Skip view if explicitly excluded.
257 0 0         if( $self->{exclude_views} ) {
258 0           for my $v ( @{ $self->{exclude_views} } ) {
  0            
259 0 0         return 1 if $name eq $v;
260              
261             # On to the next view unless this one is a wildcard.
262 0 0         next unless $v =~ m/%/;
263              
264             # Build regex by splitting view specification on '%' and replacing
265             # it with '.*'. Make sure interemediate chunks of the specification
266             # are regex quoted with qr using \Q...\E. Then add beginning and
267             # end of string anchors, '^' and '$'.
268 0           my $re = '^'.join('.*', map { qr/\Q$_\E/ } split '%', $v, -1 ).'$';
  0            
269              
270             # Ignore view if the regex matches.
271 0 0         return 1 if $name =~ $re;
272             }
273             }
274              
275             # Don't skip any more views if views list is empty.
276 0 0         return 0 unless @{ $self->{views} };
  0            
277              
278             # Skip view if not listed expliitly in views.
279 0           for my $v ( @{ $self->{views} } ) {
  0            
280 0 0         return 0 if $name eq $v;
281              
282             # On to the next view unless this one is a wildcard.
283 0 0         next unless $v =~ m/%/;
284              
285             # Build regex by splitting view specification on '%' and replacing
286             # it with '.*'. Make sure interemediate chunks of the specification
287             # are regex quoted with qr using \Q...\E. Then add beginning and end
288             # of string anchors, '^' and '$'.
289 0           my $re = '^'.join('.*', map { qr/\Q$_\E/ } split '%', $v, -1 ).'$';
  0            
290              
291             # Don't ignore view if the regex matches.
292 0 0         return 0 if $name =~ $re;
293             }
294 0           return 1;
295             }
296              
297             ### $self->__queue_sql ( $action, $desc, $sql )
298             #
299             # Queue SQL action for later execution.
300             #
301             sub __queue_sql : method
302             {
303 0     0     my $self = shift;
304 0           my( $action, $desc, $sql ) = @_;
305              
306 0           push @{$self->{todo}{$action}}, {
  0            
307             desc => $desc,
308             sql => $sql,
309             };
310             }
311              
312             sub __beautify_view
313             {
314 0     0     my $self = shift;
315 0           my($sql) = @_;
316 0           $sql =~ s/` `/` AS `/g;
317 0           $sql = $SQL_PRETTY->print($sql);
318 0           $sql =~ s/ ALGORITHM=/\n ALGORITHM=/;
319 0           $sql =~ s/ DEFINER=/\n DEFINER=/;
320 0           $sql =~ s/ SQL SECURITY /\n SQL SECURITY /;
321 0           $sql =~ s/ VIEW `([^`]+)` AS /\nVIEW `$1` AS\n/;
322 0           $sql =~ s/` *AS /` AS /g;
323 0           $sql =~ s/^(select) /SELECT\n/im;
324 0           $sql =~ s/ from \(/\nFROM (/i;
325 0           $sql .= "\n";
326 0           return $sql;
327             }
328              
329             =over 4
330              
331             =item C
332              
333             Return a list of all saved table names.
334              
335             =cut
336              
337             sub table_names : method
338             {
339 0     0 1   my $self = shift;
340 0           my @names;
341              
342 0           opendir my $dh, "$self->{dir}/table";
343 0           while( my $table_sql = readdir $dh ) {
344 0 0         my($name) = $table_sql =~ m/^(.*)\.sql$/
345             or next;
346 0           push @names, $name;
347             };
348              
349 0           return @names;
350             }
351              
352             =item $o->parse_create_table_sql ( $sql )
353              
354             Parse a CREATE TABLE statement generated by mysql "SHOW CREATE TABLE ..."
355              
356             This function is very particular about the input format.
357              
358             =cut
359              
360             sub parse_create_table_sql : method
361             {
362 0     0 1   my $self = shift;
363 0           my($sql) = @_;
364              
365 0           my @create_sql = split "\n", $sql;
366              
367             # Read the table name from the "CREATE TABLE `` (
368 0 0         shift( @create_sql ) =~ m/CREATE TABLE `(.*)`/
369             or die "Create table SQL does not begin with CREATE TABLE!\n";
370 0           my $name = $1;
371              
372             # The last line should have the table options
373             # ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='App User'
374             # We don't need to understand every last option, but let's extract at least
375             # the ENGINE and COMMENT.
376 0           my $line = pop @create_sql;
377 0           my($table_options) = $line =~ m/\)\s*(.*)/;
378              
379             # Extract the ENGINE= from the table options.
380 0 0         $table_options =~ s/ENGINE=(\S+)\s*//
381             or die "Table options lack ENGINE specification?!";
382 0           my $engine = $1;
383              
384             # Drop data about AUTO_INCREMENT
385 0           $table_options =~ s/AUTO_INCREMENT=(\d+)\s*//;
386              
387             # Extract the COMMENT and undo mysql ' quoting. We shouldn't have to deal
388             # with weird characters or backslashes in comments, so let's keep it
389             # simple.
390 0           my $comment;
391 0 0         if( $table_options =~ s/\s*COMMENT='(([^']|'')*)'// ) {
392 0           $comment = $1;
393 0           $comment =~ s/''/'/g;
394             }
395              
396             # The remaining lines should be column definitions followed by keys.
397 0           my @columns;
398             my %column_definition;
399 0           my @constraints;
400 0           my %constraint_definition;
401 0           my @keys;
402 0           my %key_definition;
403 0           my @primary_key;
404              
405 0           for my $line ( @create_sql ) {
406 0           $line =~ s/,$//; # Strip trailing commas
407              
408             # Strip out DEFAULT NULL so that it is easier to compare column
409             # definitions.
410 0           $line =~ s/ DEFAULT NULL//;
411              
412 0 0         if( $line =~ m/^\s*`([^`]+)`\s*(.*)/ ) {
    0          
    0          
    0          
413 0           my($col, $def) = ($1, $2);
414 0           push @columns, $col;
415 0           $column_definition{ $col } = $def;
416             } elsif( $line =~ m/^\s*PRIMARY KEY \(`(.*)`\)/ ) {
417 0           @primary_key = split( '`,`', $1 );
418             } elsif( $line =~ m/^\s*((UNIQUE )?KEY `([^`]+)`.*)/ ) {
419 0           my($key, $def) = ($3, $1);
420 0           push @keys, $key;
421 0           $key_definition{ $key } = $def;
422             } elsif( $line =~ m/^\s*
423             CONSTRAINT\s+`(.*)`\s+
424             FOREIGN\s+KEY\s+\(`(.*)`\)\s+
425             REFERENCES\s+`(.*)`\s+\(`(.*)`\)\s+(.*)
426             /x ) {
427 0           my($name, $cols, $reftable, $refcols, $cascade_opt) =
428             ($1, $2, $3, $4, $5);
429 0           my @cols = split '`,`', $cols;
430 0           my @refcols = split '`,`', $refcols;
431 0           push @constraints, $name;
432 0           $constraint_definition{ $name } = {
433             name => $name,
434             columns => \@cols,
435             reference_table => $reftable,
436             reference_columns => \@refcols,
437             cascade_opt => $cascade_opt,
438             };
439             } else {
440 0           warn "Don't understand line in CREATE TABLE:\n$line";
441             }
442             }
443              
444             return {
445 0           name => $name,
446             comment => $comment,
447             engine => $engine,
448             table_options => $table_options,
449             columns => \@columns,
450             column_definition => \%column_definition,
451             keys => \@keys,
452             key_definition => \%key_definition,
453             constraints => \@constraints,
454             constraint_definition => \%constraint_definition,
455             primary_key => \@primary_key,
456             };
457             }
458              
459             =item $o->read_table_sql ( $table_name )
460              
461             Given a table name, retrieve the table definition SQL.
462              
463             =cut
464              
465             sub read_table_sql : method
466             {
467 0     0 1   my $self = shift;
468 0           my($name) = @_;
469              
470             # File slurp mode.
471 0           local $/;
472              
473 0           open my $fh, "$self->{dir}/table/$name.sql";
474 0           my $sql = <$fh>;
475 0           close $fh;
476              
477 0           return $sql;
478             }
479              
480             =item $o->get_table_desc ( $table_name )
481              
482             Given a table name, retrieve the parsed table definition.
483              
484             =cut
485              
486             sub get_table_desc : method
487             {
488 0     0 1   my $self = shift;
489 0           my($name) = @_;
490              
491 0           my $sql = $self->read_table_sql( $name );
492 0           my $desc;
493 0           eval {
494 0           $desc = $self->parse_create_table_sql( $sql );
495             };
496 0 0         die "Error parsing SQL for table `$name`:\n$@" if $@;
497             die "Table name mismatch while reading SQL for `$name`, " .
498             "got `$desc->{name}` instead!\n"
499 0 0         unless $name eq $desc->{name};
500              
501 0           return $desc;
502             }
503              
504             =item $o->find_data_tables_with_revision ()
505              
506             Return table definitions that have a revision column.
507              
508             =cut
509              
510             sub find_data_tables_with_revision ($)
511             {
512 0     0 1   my $self = shift;
513              
514 0           my $archive_name_regexp = $self->{archive_name_pattern};
515 0           $archive_name_regexp =~ s/%/.*/;
516 0           $archive_name_regexp = qr/^$archive_name_regexp$/;
517              
518 0           my @tables = ();
519              
520 0           for my $name ( $self->table_names ) {
521             # Skip tables that are archive_tables
522 0 0         next if $name =~ $archive_name_regexp;
523              
524 0           my $table = $self->get_table_desc( $name );
525             push @tables, $table
526 0 0         if $table->{column_definition}{ $self->{colname}{revision} };
527             }
528              
529 0           return @tables;
530             }
531              
532             =item $o->check_table_is_archive_capable ( $table )
533              
534             Check that a table has bare minimum support required to have an archive
535             table.
536              
537             =cut
538              
539             sub check_table_is_archive_capable : method
540             {
541 0     0 1   my $self = shift;
542 0           my ( $table ) = @_;
543              
544             die "$table->{name} lacks a primary key."
545 0 0         unless @{$table->{primary_key}};
  0            
546              
547 0           my($col, $coldef);
548              
549 0           $col = $self->{colname}{revision};
550 0 0         $coldef = $table->{column_definition}{$col}
551             or die "$table->{name} lacks $col column.\n";
552 0 0         $coldef =~ m/\b(int|bigint)\b/
553             or die "$table->{name} column $col is not an integer type.\n";
554              
555 0           $col = $self->{colname}{updid};
556 0 0         if( $table->{col_definition}{$col} ) {
557 0 0         $coldef =~ m/\b(varchar)\b/
558             or die "$table->{name} column $col is not a string type.\n";
559             }
560              
561 0           for my $timecol (qw(mtime ctime)) {
562 0 0         my $col = $self->{colname}{$timecol} or next;
563 0 0         $coldef = $table->{column_definition}{$col}
564             or next;
565 0 0         $coldef =~ m/\b(timestamp|datetime)\b/
566             or die "$table->{name} column $col is neither a timestamp or " .
567             "datetime field.\n";
568             }
569              
570             # Check for obvious name conflicts.
571 0           for my $col (qw(dbuser action stmt)) {
572             die "Archive table column conflict, souce table `$table->{name}` has " .
573             "column `$col`.\n"
574 0 0         if $table->{column_definition}{$col};
575             }
576              
577             # FIXME - There are probably other engines that are okay, but which?
578             die "I can't promise this will work for table $table->{name} with " .
579             "ENGINE=$table->{engine}\n"
580 0 0         unless $table->{engine} eq 'InnoDB';
581             }
582              
583             =item $o->check_table_updatable( $current, $desired )
584              
585             Check that the current table could be updated to the desired state.
586              
587             =cut
588              
589             sub check_table_updatable : method
590             {
591 0     0 1   my $self = shift;
592 0           my( $current, $desired ) = @_;
593 0           my $name = $current->{name};
594              
595             die "Table `$name` lacks a primary key."
596 0 0         unless @{$current->{primary_key}};
  0            
597              
598 0           my $desired_pkstr = '`' . join('`,`', @{$desired->{primary_key}}) . '`';
  0            
599 0           my $current_pkstr = '`' . join('`,`', @{$current->{primary_key}}) . '`';
  0            
600 0 0         die "Table `$name` primary key is ($current_pkstr) not ($desired_pkstr)\n"
601             unless $current_pkstr eq $desired_pkstr;
602              
603             # Check for update paths between column definitions...
604 0           for my $col ( @{$desired->{columns}} ) {
  0            
605 0           my $cdef = $current->{column_definition}{$col};
606 0           my $ddef = $desired->{column_definition}{$col};
607              
608             # It should be okay to add a column... though it may fail if it is
609             # part of a unique index.
610 0 0         next unless $cdef;
611              
612 0           my $num_type = qr/^(
613             (|tiny|small|medium|big)int |
614             decimal |
615             numeric |
616             float |
617             double)\b/x;
618 0           my $datetime_type = qr/^(date|datetime|timestamp)\b/;
619 0           my $string_type = qr/^(
620             (|var)(char|binary) |
621             (|tiny|medium|long)(blob|text) |
622             enum | set)\b/x;
623 0 0         if( $ddef =~ $num_type ) {
    0          
    0          
624 0 0         $cdef =~ $num_type
625             or die "Table $name column $col is not a numeric type.\n";
626             } elsif( $ddef =~ $datetime_type ) {
627 0 0         $cdef =~ $datetime_type
628             or die "Table $name column $col is not a date or time type.\n";
629             } elsif( $ddef =~ $string_type ) {
630 0 0         $cdef =~ $string_type
631             or die "Table $name column $col is not a string type.\n";
632             }
633             }
634              
635             die "Unable Table `$current->{name}`, engine mismatch " .
636             "$current->{engine} vs. $desired->{engine}\n"
637 0 0         unless $current->{engine} eq $desired->{engine};
638             }
639              
640              
641             =item $o->make_archive_table_desc ( $table_desc )
642              
643             Make a archive table description for the given source table description.
644              
645             =cut
646              
647             sub make_archive_table_desc : method
648             {
649 0     0 1   my $self = shift;
650 0           my( $table ) = @_;
651              
652             # Use name pattern to generate archive table names.
653 0           my $name = $self->{archive_name_pattern};
654 0           $name =~ s/%/$table->{name}/;
655              
656             my %archive_table = (
657             name => $name,
658             comment => "$table->{name} archive.",
659             engine => $table->{engine},
660             table_options => $table->{table_options},
661 0           primary_key => [ @{$table->{primary_key}}, $self->{colname}{revision} ],
  0            
662             );
663              
664             # Column definitions required for audit fields.
665             my %column_definition = (
666             $self->{colname}{dbuser} =>
667             "varchar(256) NOT NULL COMMENT 'Database user & host that made " .
668             "this change.'",
669             $self->{colname}{updid} =>
670             "varchar(256) NOT NULL COMMENT 'Application user that made this " .
671             "change.'",
672             $self->{colname}{action} =>
673             "enum('insert','update','delete') NOT NULL COMMENT 'SQL action.'",
674             $self->{colname}{stmt} =>
675 0           "longtext NOT NULL COMMENT 'SQL Statement that initiated this " .
676             "change.'",
677             );
678              
679 0           my @columns;
680 0           for my $col ( @{ $table->{columns} } ) {
  0            
681 0           push @columns, $col;
682 0           my $def = $table->{column_definition}{$col};
683              
684             # Drop properties not appropriate for archive tables.
685 0           $def =~ s/ AUTO_INCREMENT//;
686              
687             # Adjust timestamp defaults and update properties to remove
688             # CURRENT_TIMESTAMP behavior.
689 0 0         if( $def =~ m/^timestamp\b/ ) {
    0          
690 0           $def =~ s/ ON UPDATE CURRENT_TIMESTAMP//;
691 0           $def =~ s/ DEFAULT CURRENT_TIMESTAMP/ DEFAULT '0000-00-00 00:00:00'/;
692 0           } elsif( ! grep { $col eq $_ } @{ $archive_table{primary_key} } ) {
  0            
693             # Allow NULL and strip defaults for columns not part of the primary
694             # key.
695 0           $def =~ s/ DEFAULT '([^']|'')+'//;
696 0           $def =~ s/ NOT NULL//;
697             }
698              
699 0           $column_definition{ $col } = $def;
700             }
701              
702             # Add columns required for archive fields.
703             # Column definitions were given above.
704 0           for my $col (qw(action updid dbuser stmt)) {
705 0           my $colname = $self->{colname}{$col};
706             # Skip columns already defined in the parent table.
707 0 0         next if $table->{column_definition}{$colname};
708 0           push @columns, $colname;
709             }
710              
711 0           $archive_table{columns} = \@columns;
712 0           $archive_table{column_definition} = \%column_definition;
713              
714 0           my @keys;
715             my %key_definition;
716 0           for my $key ( @{ $table->{keys} } ) {
  0            
717 0           push @keys, $key;
718 0           my $def = $table->{key_definition}{$key};
719              
720             # Strip unique property from keys.
721 0           $def =~ s/^UNIQUE\s*//;
722              
723 0           $key_definition{$key} = $def;
724             }
725              
726 0           $archive_table{keys} = \@keys;
727 0           $archive_table{key_definition} = \%key_definition;
728              
729 0           return \%archive_table;
730             }
731              
732             =item $o->write_table_sql( $name, $sql )
733              
734             Save create table SQL for a table.
735              
736             =cut
737              
738             sub write_table_sql : method
739             {
740 0     0 1   my $self = shift;
741 0           my( $name, $sql ) = @_;
742 0           my $fh;
743              
744             # Make table directory if required.
745 0 0         mkdir "$self->{dir}/table"
746             unless -d "$self->{dir}/table";
747              
748 0           open $fh, ">", "$self->{dir}/table/$name.sql";
749 0           print $fh $sql;
750 0           close $fh;
751             }
752              
753             =item $o->remove_table_sql( $name )
754              
755             Remove create table SQL for a table.
756              
757             =cut
758              
759             sub remove_table_sql : method
760             {
761 0     0 1   my $self = shift;
762 0           my( $name ) = @_;
763 0           unlink "$self->{dir}/table/$name.sql";
764             }
765              
766             =item $o->write_table_definition( $table )
767              
768             Write create table SQL for given table description.
769              
770             =cut
771              
772             sub write_table_definition : method
773             {
774 0     0 1   my $self = shift;
775 0           my( $table ) = @_;
776              
777 0           my $sql = "CREATE TABLE `$table->{name}` (\n";
778              
779 0           for my $col ( @{ $table->{columns} } ) {
  0            
780 0           $sql .= " `$col` $table->{column_definition}{$col},\n";
781             }
782              
783 0           for my $key ( @{ $table->{keys} } ) {
  0            
784 0           $sql .= " $table->{key_definition}{$key},\n";
785             }
786              
787             # Quote in a lazy way... to do it proper would require a database
788             # connection.
789 0   0       my $comment = $table->{comment} || $table->{name};
790 0           $comment =~ s/'/''/g;
791              
792             $sql .= " PRIMARY KEY (`" .
793 0           join('`,`', @{$table->{primary_key}} ) .
  0            
794             "`)\n";
795 0           $sql .= ") ENGINE=$table->{engine} $table->{table_options} " .
796             "COMMENT='$comment'\n";
797              
798 0           $self->write_table_sql( $table->{name}, $sql );
799             }
800              
801             =item $o->remove_trigger_fragment( $fragment )
802              
803             Remove trigger fragment SQL.
804              
805             =cut
806              
807             sub remove_trigger_fragment : method
808             {
809 0     0 1   my $self = shift;
810 0           my( $fragment ) = @_;
811 0           unlink "$self->{dir}/trigger/$fragment->{file}";
812             }
813              
814             =item $o->write_trigger_fragment_sql( $name, $time, $action, $table, $sql )
815              
816             Write trigger fragement SQL to a file.
817              
818             =cut
819              
820             sub write_trigger_fragment_sql : method
821             {
822 0     0 1   my $self = shift;
823 0           my( $name, $time, $action, $table, $sql ) = @_;
824 0           my $fh;
825              
826             # Make trigger directory if required.
827 0 0         mkdir "$self->{dir}/trigger"
828             unless -d "$self->{dir}/trigger";
829              
830 0           open $fh, ">", "$self->{dir}/trigger/$name.$time.$action.$table.sql";
831 0           print $fh $sql;
832 0           close $fh;
833             }
834              
835             =item $o->write_archive_trigger_fragments( $table, $archive_table_desc )
836              
837             Write trigger fragment sql for archive table management.
838              
839             =cut
840              
841             sub write_archive_trigger_fragments : method
842             {
843 0     0 1   my $self = shift;
844 0           my( $table, $archive_table ) = @_;
845 0           my $colname = $self->{colname};
846 0           my $fragment;
847             my $fh;
848              
849             # Make trigger directory if required.
850 0 0         mkdir "$self->{dir}/trigger"
851             unless -d "$self->{dir}/trigger";
852              
853              
854             # Before insert
855             #$fragment = "SET NEW.`$colname->{revision}` = 0;\n";
856             $fragment =
857             "SET NEW.`$colname->{revision}` = (\n" .
858             " SELECT IFNULL( MAX(`$colname->{revision}`) + 1, 0 )\n" .
859             " FROM `$archive_table->{name}`\n" .
860             " WHERE " .
861             join(" AND ", map {
862 0           "`$_` = NEW.`$_`"
863 0           } @{$table->{primary_key}}) .
  0            
864             "\n);\n";
865             $fragment .= "SET NEW.`$colname->{ctime}` = CURRENT_TIMESTAMP;\n"
866 0 0 0       if $colname->{ctime} and $table->{column_definition}{$colname->{ctime}};
867             $fragment .= "SET NEW.`$colname->{mtime}` = CURRENT_TIMESTAMP;\n"
868 0 0 0       if $colname->{mtime} and $table->{column_definition}{$colname->{mtime}};
869             $fragment .= "SET NEW.`$colname->{updid}` = $self->{updidvar};\n"
870 0 0         if $table->{column_definition}{ $colname->{updid} };
871             $self->write_trigger_fragment_sql(
872 0           "20-archive", "before", "insert", $table->{name}, $fragment
873             );
874              
875              
876             # Before update
877 0           $fragment =
878             "SET NEW.`$colname->{revision}` = OLD.`$colname->{revision}` + 1;\n";
879             $fragment .= "SET NEW.`$colname->{ctime}` = OLD.`$colname->{ctime}`;\n"
880 0 0 0       if $colname->{ctime} and $table->{column_definition}{$colname->{ctime}};
881             $fragment .= "SET NEW.`$colname->{mtime}` = CURRENT_TIMESTAMP;\n"
882 0 0 0       if $colname->{mtime} and $table->{column_definition}{$colname->{mtime}};
883             $fragment .= "SET NEW.`$colname->{updid}` = $self->{updidvar};\n"
884 0 0         if $table->{column_definition}{ $colname->{updid} };
885             $self->write_trigger_fragment_sql(
886 0           "20-archive", "before", "update", $table->{name}, $fragment
887             );
888              
889              
890             # Columns that don't receive special treatment.
891             # Exclude columns with special names.
892             my %namecol = map {
893 0 0         $colname->{$_} ? ($colname->{$_} => $_) : ()
  0            
894             } keys %$colname;
895 0           my @cols = grep { not $namecol{$_} } @{ $table->{columns} };
  0            
  0            
896              
897             # Special columns
898 0           my @scols = grep { $colname->{$_} } sort keys %$colname;
  0            
899             # Drop handling of ctime if not is table.
900 0 0 0       if( $colname->{ctime}
901             and not $table->{column_definition}{$colname->{ctime}}
902             ) {
903 0           @scols = grep { $_ ne 'ctime' } @scols;
  0            
904             }
905              
906             $fragment =
907             "BEGIN DECLARE stmt longtext;\n" .
908             "SET stmt = ( SELECT info FROM INFORMATION_SCHEMA.PROCESSLIST " .
909             "WHERE id = CONNECTION_ID() );\n" .
910             "INSERT INTO `$archive_table->{name}` (\n" .
911 0           " `" . join( '`, `', @cols, map { $colname->{$_} } @scols ) . "`\n".
  0            
912             ") VALUES (\n";
913              
914             # After insert
915             $self->write_trigger_fragment_sql(
916             "40-archive", "after", "insert", $table->{name},
917             $fragment . " NEW.`" . join('`, NEW.`', @cols) . "`,\n" .
918             " " . join(', ', map {
919 0           m/^(ctime|mtime|revision)$/ ? "NEW.`$colname->{$_}`" :
920             $_ eq 'action' ? "'insert'" :
921             $_ eq 'updid' ? $self->{updidvar} :
922 0 0         $_ eq 'dbuser' ? "USER()" :
    0          
    0          
    0          
    0          
923             $_ eq 'stmt' ? 'stmt' : die "BUG! $_ unhandled!"
924             } @scols) . "\n);\nEND;\n"
925             );
926              
927             # After update
928             $self->write_trigger_fragment_sql(
929             "40-archive", "after", "update", $table->{name},
930             $fragment . " NEW.`" . join('`, NEW.`', @cols) . "`,\n" .
931             " " . join(', ', map {
932 0           m/^(ctime|mtime|revision)$/ ? "NEW.`$colname->{$_}`" :
933             $_ eq 'action' ? "'update'" :
934             $_ eq 'updid' ? $self->{updidvar} :
935 0 0         $_ eq 'dbuser' ? "USER()" :
    0          
    0          
    0          
    0          
936             $_ eq 'stmt' ? 'stmt' : die "BUG! $_ unhandled!"
937             } @scols) . "\n);\nEND;\n"
938             );
939              
940             # After delete
941             $self->write_trigger_fragment_sql(
942             "40-archive", "after", "delete", $table->{name},
943             $fragment . " OLD.`" . join('`, OLD.`', @cols) . "`,\n" .
944             " " . join(', ', map {
945 0           $_ eq 'action' ? "'delete'" :
946             $_ eq 'updid' ? $self->{updidvar} :
947 0 0         $_ eq 'ctime' ? "OLD.`$colname->{ctime}`" :
    0          
    0          
    0          
    0          
    0          
    0          
948             $_ eq 'dbuser' ? "USER()" :
949             $_ eq 'mtime' ? "CURRENT_TIMESTAMP" :
950             $_ eq 'revision' ? "1 + OLD.`$colname->{revision}`" :
951             $_ eq 'stmt' ? 'stmt' : die "BUG! $_ unhandled!"
952             } @scols) . "\n);\nEND;\n"
953             );
954              
955             }
956              
957             =item $o->query_table_sql ( $name )
958              
959             =cut
960              
961             sub query_table_sql : method
962             {
963 0     0 1   my $self = shift;
964 0           my( $name ) = @_;
965 0           my $dbh = $self->{dbh};
966              
967 0           my $sth = $dbh->prepare( "SHOW CREATE TABLE `$name`" );
968 0           $sth->execute();
969 0           my @row = $sth->fetchrow_array;
970              
971 0           return "$row[1]\n";
972             }
973              
974             =item $o->pull_table_definition ( $name )
975              
976             =cut
977              
978             sub pull_table_definition : method
979             {
980 0     0 1   my $self = shift;
981 0           my( $name ) = @_;
982              
983 0 0         print "Pulling table definition for `$name`\n" if $VERBOSE;
984              
985             # Get MySQL create table sql
986 0           my $sql = $self->query_table_sql( $name );
987              
988             # Parse create table sql to local representation.
989 0           my $table = $self->parse_create_table_sql( $sql );
990              
991             # Regenerate SQL from local representation.
992 0           $sql = $self->create_table_sql( $table );
993              
994             # Save table sql.
995 0           $self->write_table_sql( $name, $sql );
996             }
997              
998             =item $o->pull_table_definitions ()
999              
1000             =cut
1001              
1002             sub pull_table_definitions : method
1003             {
1004 0     0 1   my $self = shift;
1005 0           my $dbh = $self->{dbh};
1006              
1007             # Make table directory if required.
1008 0 0         mkdir "$self->{dir}/table"
1009             unless -d "$self->{dir}/table";
1010              
1011             # Variable to keep track of tables in the database.
1012 0           my %db_table = ();
1013              
1014 0           for my $name ( $self->query_table_names ) {
1015              
1016 0 0         next if $self->__ignore_table( $name );
1017              
1018 0           $db_table{ $name } = 1;
1019 0           pull_table_definition( $self, $name );
1020             }
1021              
1022 0 0         if( $self->{remove}{table} ) {
1023 0           for my $name ( $self->table_names ) {
1024 0 0         next if $self->__ignore_table( $name );
1025              
1026             # Don't remove this table, it was found in the database.
1027 0 0         next if $db_table{$name};
1028              
1029 0           $self->remove_table_sql( $name );
1030             }
1031             }
1032             }
1033              
1034             =item $o->queue_create_table ( $table )
1035              
1036             =cut
1037              
1038             sub queue_create_table : method
1039             {
1040 0     0 1   my $self = shift;
1041 0           my( $table ) = @_;
1042              
1043 0           $self->__queue_sql( 'create_table',
1044             "Create table $table->{name}.",
1045             $self->create_table_sql( $table, { no_constraints => 1 } ),
1046             );
1047              
1048 0           for my $constraint ( @{$table->{constraints}} ) {
  0            
1049 0           $self->queue_add_table_constraint($table, $constraint);
1050             }
1051             }
1052              
1053             =item $o->create_table_sql ( $table )
1054              
1055             =cut
1056              
1057             sub create_table_sql : method
1058             {
1059 0     0 1   my $self = shift;
1060 0           my( $table, $opt ) = @_;
1061              
1062 0           my $sql = "CREATE TABLE `$table->{name}` (\n";
1063              
1064 0           for my $col ( @{ $table->{columns} } ) {
  0            
1065 0           $sql .= " `$col` $table->{column_definition}{$col},\n";
1066             }
1067              
1068 0           for my $key ( sort @{ $table->{keys} } ) {
  0            
1069 0           $sql .= " $table->{key_definition}{$key},\n";
1070             }
1071              
1072 0 0         unless( $opt->{no_constraints} ) {
1073 0           for my $constraint ( sort @{$table->{constraints}} ) {
  0            
1074             $sql .= " " . $self->constraint_sql(
1075 0           $table->{constraint_definition}{$constraint}
1076             ) . ",\n";
1077             }
1078             }
1079              
1080 0           $sql .= " PRIMARY KEY (`" . join('`,`', @{$table->{primary_key}}) . "`)\n";
  0            
1081 0           $sql .= ") ENGINE=$table->{engine} $table->{table_options}";
1082 0 0         if( $table->{comment} ) {
1083 0           my $comment = $table->{comment};
1084 0           $comment =~ s/'/''/g;
1085 0           $sql .= " COMMENT='$comment'";
1086             }
1087 0           $sql .= "\n";
1088              
1089 0           return $sql;
1090             }
1091              
1092             =item $o->constraint_sql ( $constraint )
1093              
1094             =cut
1095              
1096             sub constraint_sql : method
1097             {
1098 0     0 1   my $self = shift;
1099 0           my($constraint) = @_;
1100             return "CONSTRAINT `$constraint->{name}` FOREIGN KEY (`"
1101 0           . join('`,`', @{$constraint->{columns}})
1102             . "`) REFERENCES `$constraint->{reference_table}` (`"
1103 0           . join('`,`', @{$constraint->{reference_columns}})
1104             . "`)" . (
1105 0 0         $constraint->{cascade_opt} ? " $constraint->{cascade_opt}" : ''
1106             );
1107             }
1108              
1109             =item $o->queue_add_table_constraint ( $table, $constraint )
1110              
1111             =cut
1112              
1113             sub queue_add_table_constraint : method
1114             {
1115 0     0 1   my $self = shift;
1116 0           my($table, $constraint) = @_;
1117              
1118 0           my $def = $table->{constraint_definition}{$constraint};
1119              
1120 0           $self->__queue_sql( 'add_constraint',
1121             "Add constraint $constraint on $table->{name}.",
1122             "ALTER TABLE `$table->{name}` ADD ".$self->constraint_sql( $def ),
1123             );
1124              
1125 0           return $self;
1126             }
1127              
1128             =item $o->queue_drop_table_constraint ( $table, $constraint )
1129              
1130             =cut
1131              
1132             sub queue_drop_table_constraint : method
1133             {
1134 0     0 1   my $self = shift;
1135 0           my($table, $constraint) = @_;
1136              
1137 0           $self->__queue_sql( 'drop_constraint',
1138             "Drop constraint $constraint on $table->{name}.",
1139             "ALTER TABLE `$table->{name}` DROP FOREIGN KEY `$constraint`",
1140             );
1141              
1142 0           return $self;
1143             }
1144              
1145             =item $o->queue_table_updates( $current, $desired )
1146              
1147             =cut
1148              
1149             sub queue_table_updates : method
1150             {
1151 0     0 1   my $self = shift;
1152 0           my($current, $new) = @_;
1153              
1154 0           for( my $i=0; $i < @{ $new->{columns} }; ++$i ) {
  0            
1155 0           my $col = $new->{columns}[$i];
1156 0 0         if( $current->{column_definition}{$col} ) {
1157 0 0         unless( $current->{column_definition}{$col}
1158             eq $new->{column_definition}{$col}
1159             ) {
1160 0           $self->__queue_sql( 'modify_column',
1161             "Modify column $col in $current->{name}\n",
1162             "ALTER TABLE `$current->{name}` " .
1163             "MODIFY COLUMN `$col` $new->{column_definition}{$col}",
1164             );
1165             }
1166             } else {
1167             $self->__queue_sql( 'add_column',
1168             "Add column $col to $current->{name}.",
1169             "ALTER TABLE `$current->{name}`" .
1170             " ADD COLUMN `$col` $new->{column_definition}{$col} " .
1171             ( $i == 0 ?
1172             + "BEFORE `$new->{columns}[1]`"
1173 0 0         : "AFTER `".$new->{columns}[$i-1]."`"
1174             )
1175             );
1176             }
1177             }
1178              
1179             # Look for unmatched columns to drop if drop_tables is set.
1180 0 0         if( $self->{drop_columns} ) {
1181 0           for my $col ( @{ $current->{columns} } ) {
  0            
1182 0 0         next if $new->{column_definition}{$col};
1183              
1184 0           $self->__queue_sql( 'drop_column',
1185             "Drop column $col from $current->{name}.",
1186             "ALTER TABLE `$current->{name}` DROP COLUMN `$col`",
1187             );
1188             }
1189             }
1190              
1191 0           for my $key ( @{ $new->{keys} } ) {
  0            
1192 0           my $new_keydef = $new->{key_definition}{$key};
1193 0           my $current_keydef = $current->{key_definition}{$key};
1194              
1195 0 0         if( $current_keydef ) {
1196 0 0         unless( $current_keydef eq $new_keydef ) {
1197 0           $self->__queue_sql( 'drop_key',
1198             "Drop key $key on $current->{name}.",
1199             "ALTER TABLE `$current->{name}` DROP KEY `$key`",
1200             );
1201 0           $self->__queue_sql( 'add_key',
1202             "Add key $key on $current->{name}.",
1203             "ALTER TABLE `$current->{name}` ADD $new_keydef",
1204             );
1205             }
1206             } else {
1207 0           $self->__queue_sql( 'add_key',
1208             "Create key $key on $current->{name}.",
1209             "ALTER TABLE `$current->{name}` ADD $new_keydef",
1210             );
1211             }
1212             }
1213              
1214 0           for my $constraint ( @{$new->{constraints}} ) {
  0            
1215 0 0 0       if( ! $current->{constraint_definition}{$constraint}
1216             or freeze($current->{constraint_definition}{$constraint})
1217             ne freeze($new->{constraint_definition}{$constraint})
1218             ) {
1219             $self->queue_drop_table_constraint($current, $constraint)
1220 0 0         if $current->{constraint_definition}{$constraint};
1221 0           $self->queue_add_table_constraint($new, $constraint);
1222             }
1223             }
1224 0           for my $constraint ( @{$current->{constraints}} ) {
  0            
1225 0 0         next if $new->{constraint_definition}{$constraint};
1226 0           $self->queue_drop_table_constraint($current, $constraint);
1227             }
1228             }
1229              
1230             =item $o->push_table_definition( $table )
1231              
1232             =cut
1233              
1234             sub queue_push_table_definition : method
1235             {
1236 0     0 0   my $self = shift;
1237 0           my($name) = @_;
1238              
1239 0           my $new_sql = $self->read_table_sql( $name );
1240 0           my $new = $self->parse_create_table_sql( $new_sql );
1241              
1242 0           my( $current, $current_sql );
1243 0           eval {
1244 0           $current_sql = $self->query_table_sql( $name );
1245 0           $current = $self->parse_create_table_sql( $current_sql );
1246             };
1247              
1248 0 0         if( $current ) {
1249 0           $self->queue_table_updates( $current, $new );
1250             } else {
1251 0           $self->queue_create_table( $new );
1252             }
1253              
1254             }
1255              
1256             =item $o->push_table_definitions()
1257              
1258             =cut
1259              
1260             sub queue_push_table_definitions : method
1261             {
1262 0     0 0   my $self = shift;
1263              
1264 0           my @tables = $self->table_names;
1265              
1266 0           for my $name ( @tables ) {
1267              
1268 0 0         next if $self->__ignore_table( $name );
1269              
1270 0           $self->queue_push_table_definition( $name );
1271             }
1272              
1273 0 0         if( $self->{remove}{table} ) {
1274 0           for my $name ( $self->query_table_names ) {
1275              
1276 0 0         next if $self->__ignore_table( $name );
1277              
1278             # Skip tables that are defined locally
1279 0 0         next if grep { $name eq $_ } @tables;
  0            
1280              
1281 0           $self->queue_drop_table( $name );
1282             }
1283             }
1284             }
1285              
1286             =item $o->queue_drop_table ( $name )
1287              
1288             =cut
1289              
1290             sub queue_drop_table : method
1291             {
1292 0     0 1   my $self = shift;
1293 0           my($name) = @_;
1294 0           $self->__queue_sql( 'drop_table',
1295             "Drop table $name\n",
1296             "DROP TABLE `$name`",
1297             );
1298             }
1299              
1300              
1301             ### VIEW ###
1302              
1303             =item C
1304              
1305             Return a list of all saved view names.
1306              
1307             =cut
1308              
1309             sub view_names : method
1310             {
1311 0     0 1   my $self = shift;
1312 0           my @names;
1313              
1314 0           my $viewdir = "$self->{dir}/view";
1315 0 0         return () unless -d $viewdir;
1316              
1317 0           my $dh;
1318 0           opendir $dh, $viewdir;
1319 0           for my $view_sql ( sort readdir $dh ) {
1320 0 0         my($name) = $view_sql =~ m/^(.*)\.sql$/
1321             or next;
1322 0           $name =~ s/^\d\d-//;
1323 0           push @names, $name;
1324             };
1325              
1326 0           return @names;
1327             }
1328              
1329             =item $o->read_view_sql ( $table_name )
1330              
1331             Given a table name, retrieve the table definition SQL.
1332              
1333             =cut
1334              
1335             sub read_view_sql : method
1336             {
1337 0     0 1   my $self = shift;
1338 0           my($name) = @_;
1339              
1340             # File slurp mode.
1341 0           local $/;
1342              
1343             # Look for file with numeric prefix.
1344 0           my($file) = glob "$self->{dir}/view/[0-9][0-9]-$name.sql";
1345 0   0       $file ||= "$self->{dir}/view/$name.sql";
1346 0           open(my $fh, $file);
1347 0           my $sql = <$fh>;
1348 0           close $fh;
1349              
1350 0           return $sql;
1351             }
1352              
1353             =item $o->write_view_sql( $name, $sql )
1354              
1355             Save create view SQL for a view.
1356              
1357             =cut
1358              
1359             sub write_view_sql : method
1360             {
1361 0     0 1   my $self = shift;
1362 0           my( $name, $sql ) = @_;
1363 0           my $fh;
1364              
1365             # Make view directory if required.
1366 0 0         mkdir "$self->{dir}/view"
1367             unless -d "$self->{dir}/view";
1368              
1369 0           my($file) = glob "$self->{dir}/view/[0-9][0-9]-$name.sql";
1370 0   0       $file ||= "$self->{dir}/view/$name.sql";
1371              
1372 0           open $fh, ">", $file;
1373 0           print $fh $sql;
1374 0           close $fh;
1375             }
1376              
1377             =item $o->remove_view_sql( $name )
1378              
1379             Remove create view SQL for a view.
1380              
1381             =cut
1382              
1383             sub remove_view_sql : method
1384             {
1385 0     0 1   my $self = shift;
1386 0           my( $name ) = @_;
1387 0           unlink "$self->{dir}/view/$name.sql";
1388             }
1389              
1390             =item $o->query_table_names ()
1391              
1392             =cut
1393              
1394             sub query_table_names : method
1395             {
1396 0     0 1   my $self = shift;
1397 0           my $dbh = $self->{dbh};
1398              
1399 0           my $sth = $dbh->prepare( "SHOW FULL TABLES WHERE Table_type='BASE TABLE'" );
1400 0           $sth->execute();
1401              
1402 0           my @tables = ();
1403 0           while( my($name) = $sth->fetchrow_array ) {
1404 0           push @tables, $name;
1405             }
1406              
1407 0           return @tables;
1408             }
1409              
1410             =item $o->query_view_sql ( $name )
1411              
1412             =cut
1413              
1414             sub query_view_sql : method
1415             {
1416 0     0 1   my $self = shift;
1417 0           my( $name ) = @_;
1418 0           my $dbh = $self->{dbh};
1419              
1420 0           my $sth = $dbh->prepare( "SHOW CREATE VIEW `$name`" );
1421 0           $sth->execute();
1422 0           my @row = $sth->fetchrow_array;
1423              
1424 0           return $self->__beautify_view($row[1]);
1425             }
1426              
1427             =item $o->pull_view_definition ( $name )
1428              
1429             =cut
1430              
1431             sub pull_view_definition : method
1432             {
1433 0     0 1   my $self = shift;
1434 0           my( $name ) = @_;
1435              
1436 0 0         print "Pulling view definition for `$name`\n" if $VERBOSE;
1437              
1438             # Get MySQL create view sql
1439 0           my $sql = $self->query_view_sql( $name );
1440              
1441             # Save view sql.
1442 0           $self->write_view_sql( $name, $sql );
1443             }
1444              
1445             =item $o->query_view_names ()
1446              
1447             =cut
1448              
1449             sub query_view_names : method
1450             {
1451 0     0 1   my $self = shift;
1452 0           my $dbh = $self->{dbh};
1453              
1454 0           my $sth = $dbh->prepare( "SHOW FULL TABLES WHERE Table_type='VIEW'" );
1455 0           $sth->execute();
1456              
1457 0           my @views = ();
1458 0           while( my($name) = $sth->fetchrow_array ) {
1459 0           push @views, $name;
1460             }
1461              
1462 0           return @views;
1463             }
1464              
1465             =item $o->queue_create_view( $new_sql )
1466              
1467             =cut
1468              
1469             sub queue_create_view : method
1470             {
1471 0     0 1   my $self = shift;
1472 0           my( $name, $sql ) = @_;
1473 0           $sql =~ s/^CREATE/CREATE OR REPLACE/i;
1474              
1475 0           $self->__queue_sql( 'create_view',
1476             "Create view $name.",
1477             $sql
1478             );
1479             }
1480              
1481             =item $o->push_view_definition( $view )
1482              
1483             =cut
1484              
1485             sub queue_push_view_definition : method
1486             {
1487 0     0 0   my $self = shift;
1488 0           my($name) = @_;
1489              
1490 0           my $new_sql = $self->read_view_sql( $name );
1491              
1492 0           my( $current_sql );
1493 0           eval {
1494 0           $current_sql = $self->query_view_sql( $name );
1495             };
1496              
1497 0 0 0       if( $current_sql and $current_sql ne $new_sql) {
1498 0           $self->queue_drop_view( $name );
1499             }
1500              
1501 0           $self->queue_create_view( $name, $new_sql );
1502             }
1503              
1504             =item $o->push_view_definitions()
1505              
1506             =cut
1507              
1508             sub queue_push_view_definitions : method
1509             {
1510 0     0 0   my $self = shift;
1511              
1512 0           my @views = $self->view_names;
1513              
1514 0           for my $name ( @views ) {
1515              
1516 0 0         next if $self->__ignore_view( $name );
1517              
1518 0           $self->queue_push_view_definition( $name );
1519             }
1520              
1521 0 0         if( $self->{remove}{view} ) {
1522 0           for my $name ( $self->query_view_names ) {
1523              
1524 0 0         next if $self->__ignore_view( $name );
1525              
1526             # Skip views that are defined locally
1527 0 0         next if grep { $name eq $_ } @views;
  0            
1528              
1529 0           $self->queue_drop_view( $name );
1530             }
1531             }
1532             }
1533              
1534             =item $o->queue_drop_view ( $name )
1535              
1536             =cut
1537              
1538             sub queue_drop_view : method
1539             {
1540 0     0 1   my $self = shift;
1541 0           my($name) = @_;
1542 0           $self->__queue_sql( 'drop_view',
1543             "Drop view $name\n",
1544             "DROP VIEW `$name`",
1545             );
1546             }
1547              
1548             =item $o->pull_view_definitions ()
1549              
1550             =cut
1551              
1552             sub pull_view_definitions : method
1553             {
1554 0     0 1   my $self = shift;
1555 0           my $dbh = $self->{dbh};
1556              
1557             # Make view directory if required.
1558 0 0         mkdir "$self->{dir}/view"
1559             unless -d "$self->{dir}/view";
1560              
1561             # Variable to keep track of views in the database.
1562 0           my %db_view = ();
1563              
1564 0           for my $name ( $self->query_view_names ) {
1565              
1566 0 0         next if $self->__ignore_view( $name );
1567              
1568 0           $db_view{ $name } = 1;
1569 0           pull_view_definition( $self, $name );
1570             }
1571              
1572 0 0         if( $self->{remove}{view} ) {
1573 0           for my $name ( $self->view_names ) {
1574 0 0         next if $self->__ignore_view( $name );
1575              
1576             # Don't remove this view, it was found in the database.
1577 0 0         next if $db_view{$name};
1578              
1579 0           $self->remove_view_sql( $name );
1580             }
1581             }
1582             }
1583              
1584             ### TRIGGER ###
1585              
1586             =item $o->trigger_fragments ()
1587              
1588             Return list of trigger fragment names.
1589              
1590             =cut
1591              
1592             sub trigger_fragments : method
1593             {
1594 0     0 1   my $self = shift;
1595              
1596 0           my $dir = "$self->{dir}/trigger";
1597 0 0         return () unless -d $dir;
1598 0           my $dh;
1599 0           opendir $dh, $dir;
1600              
1601 0           my @fragments = ();
1602 0           for my $file ( sort readdir $dh ) {
1603 0 0         my($name, $time, $action, $table) =
1604             $file =~ m/^
1605             (.+)\.(before|after)\.(insert|update|delete)\.(.+)\.sql$
1606             /x or next;
1607 0           push @fragments, {
1608             action => $action,
1609             file => $file,
1610             name => $name,
1611             table => $table,
1612             time => $time,
1613             };
1614             }
1615              
1616 0           return @fragments;
1617             }
1618              
1619             =item $o->assemble_triggers ()
1620              
1621             Assemble trigger fragments into nested hash of triggers.
1622              
1623             =cut
1624              
1625             sub assemble_triggers : method
1626             {
1627 0     0 1   my $self = shift;
1628              
1629 0           my %triggers = ();
1630 0           for my $fragment ( $self->trigger_fragments ) {
1631              
1632 0           my $sql = $self->read_trigger_fragment_sql( $fragment );
1633             my($table, $action, $time, $name) =
1634 0           @{$fragment}{'table', 'action', 'time', 'name'};
  0            
1635              
1636 0   0       $triggers{$table}{$action}{$time} ||= '';
1637 0           $triggers{$table}{$action}{$time} .=
1638             "/** begin $name */\n$sql/** end $name */\n";
1639             }
1640              
1641 0           return %triggers;
1642             }
1643              
1644             =item $o->read_trigger_fragment_sql ( \%fragment )
1645              
1646             =cut
1647              
1648             sub read_trigger_fragment_sql : method
1649             {
1650 0     0 1   my $self = shift;
1651 0           my( $fragment ) = @_;
1652              
1653             # Slurp file.
1654 0           local $/;
1655 0           my $dir = "$self->{dir}/trigger";
1656 0           open my $fh, "<", "$dir/$fragment->{file}";
1657 0           my $sql = <$fh>;
1658 0           close $fh;
1659              
1660 0           return $sql;
1661             }
1662              
1663             =item $o->queue_push_trigger_definitions()
1664              
1665             =cut
1666              
1667             sub queue_push_trigger_definitions : method
1668             {
1669 0     0 1   my $self = shift;
1670              
1671 0           my %triggers = $self->assemble_triggers();
1672 0           my %current_triggers = $self->pull_trigger_definitions();
1673              
1674 0           for my $table ( sort keys %triggers ) {
1675              
1676 0 0         next if $self->__ignore_table( $table );
1677              
1678 0           for my $action ( sort keys %{$triggers{$table}} ) {
  0            
1679 0           for my $time ( sort keys %{$triggers{$table}{$action}} ) {
  0            
1680 0           my $new = $triggers{$table}{$action}{$time};
1681 0           my $current = $current_triggers{$table}{$action}{$time};
1682              
1683 0           my $create_sql =
1684             "CREATE TRIGGER `${time}_${action}_${table}` " .
1685             "$time $action ON `$table` FOR EACH ROW BEGIN\n${new}END";
1686              
1687 0 0         if( not $current ) {
    0          
1688 0           $self->__queue_sql( 'create_trigger',
1689             "Create $time $action on $table trigger.",
1690             $create_sql,
1691             );
1692             } elsif( $current->{sql} ne $new ) {
1693 0           $self->__queue_sql( 'drop_trigger',
1694             "Drop $time $action on $table trigger.",
1695             "DROP TRIGGER IF EXISTS `$current->{name}`",
1696             );
1697 0           $self->__queue_sql( 'create_trigger',
1698             "Create $time $action on $table trigger.",
1699             $create_sql,
1700             );
1701             }
1702             }
1703             }
1704             }
1705              
1706             # Check if any triggers should be dropped.
1707 0 0         if( $self->{remove}{trigger} ) {
1708 0           for my $table ( sort keys %current_triggers ) {
1709              
1710 0 0         next if $self->__ignore_table( $table );
1711              
1712 0           for my $action ( sort keys %{$current_triggers{$table}} ) {
  0            
1713 0           for my $time (
1714 0           sort keys %{$current_triggers{$table}{$action}}
1715             ) {
1716 0 0         next if $triggers{$table}{$action}{$time};
1717 0           my $trigger = $current_triggers{$table}{$action}{$time};
1718              
1719 0           $self->__queue_sql( 'drop_trigger',
1720             "Drop $time $action on $table trigger.",
1721             "DROP TRIGGER IF EXISTS `$trigger->{name}`",
1722             );
1723             }
1724             }
1725             }
1726             }
1727             }
1728              
1729             =item $o->pull_trigger_definitions ()
1730              
1731             =cut
1732              
1733             sub pull_trigger_definitions : method
1734             {
1735 0     0 1   my $self = shift;
1736 0           my $dbh = $self->{dbh};
1737              
1738 0           my $list_sth = $dbh->prepare( 'SHOW TRIGGERS' );
1739 0           $list_sth->execute();
1740              
1741 0           my %triggers;
1742 0           while( my($trigger_name, $action, $table, $sql, $time) =
1743             $list_sth->fetchrow_array()
1744             ) {
1745 0 0         next if $self->__ignore_table( $table );
1746              
1747             # Strip off BEGIN and END from trigger body
1748 0           $sql =~ s/^\s*BEGIN\s*(.*)END\s*$/$1/s;
1749              
1750             # Lowercase is easier to read
1751 0           $action = lc $action;
1752 0           $time = lc $time;
1753              
1754 0           $triggers{$table}{$action}{$time} = {
1755             sql => $sql,
1756             name => $trigger_name,
1757             };
1758             }
1759              
1760 0           return %triggers;
1761             }
1762              
1763             =item $o->pull_trigger_fragments : method
1764              
1765             =cut
1766              
1767             sub pull_trigger_fragments : method
1768             {
1769 0     0 1   my($self) = @_;
1770              
1771 0           my %triggers = pull_trigger_definitions( $self );
1772              
1773             # Variable to track fragments.
1774 0           my %found_fragments = ();
1775              
1776 0           for my $table ( sort keys %triggers ) {
1777              
1778 0 0         next if $self->__ignore_table( $table );
1779              
1780 0           for my $action ( sort keys %{$triggers{$table}} ) {
  0            
1781 0           for my $time ( sort keys %{$triggers{$table}{$action}} ) {
  0            
1782 0           my $trigger_sql = $triggers{$table}{$action}{$time}{sql};
1783              
1784             # Parse all tagged trigger fragments
1785 0           while( $trigger_sql =~ s{
1786             /\*\*\s+begin\s+(\S+)\s+\*/\s*(.*)/\*\*\s+end\s+\1\s+\*/\s*
1787             }{}sx ) {
1788 0           my( $name, $sql ) = ($1, $2);
1789 0           $self->write_trigger_fragment_sql(
1790             $name, $time, $action, $table, $sql
1791             );
1792 0           $found_fragments{$table}{$action}{$time}{$name} = 1;
1793             }
1794              
1795             # Handle any untagged trigger SQL?
1796 0           $trigger_sql =~ s/\s*$//;
1797 0 0         if( $trigger_sql ) {
1798 0 0         if( $self->{init_trigger_name} ) {
1799 0           my $name = $self->{init_trigger_name};
1800 0           $self->write_trigger_fragment_sql(
1801             $name, $time, $action, $table, $trigger_sql
1802             );
1803 0           $found_fragments{$table}{$action}{$time}{$name} = 1;
1804             } else {
1805 0           die "Found unlabeled trigger code for " .
1806             "$time $action `$table`!\n$trigger_sql\n" .
1807             "Do you need to specify --init-trigger-name=NAME?\n";
1808             }
1809             }
1810             }
1811             }
1812             }
1813              
1814             # Remove trigger fragment not found during pull.
1815 0 0         if( $self->{remove}{trigger} ) {
1816 0           for my $fragment ( $self->trigger_fragments ) {
1817             my($table, $action, $time, $name) =
1818 0           @{$fragment}{'table', 'action', 'time', 'name'};
  0            
1819 0 0         next if $found_fragments{$table}{$action}{$time}{$name};
1820              
1821 0           $self->remove_trigger_fragment( $fragment );
1822             }
1823             }
1824             }
1825              
1826             ### PROCEDURE ###
1827              
1828             =item $o->procedure_names()
1829              
1830             =cut
1831              
1832             sub procedure_names
1833             {
1834 0     0 1   my $self = shift;
1835 0           my @names;
1836 0 0         return () unless -d "$self->{dir}/procedure";
1837              
1838 0           opendir my $dh, "$self->{dir}/procedure";
1839 0           while( my $sql = readdir $dh ) {
1840 0 0         my($name) = $sql =~ m/^(.*)\.sql$/
1841             or next;
1842 0           push @names, $name;
1843             };
1844              
1845 0           return @names;
1846             }
1847              
1848             =item $o->read_procedure_sql ( $name )
1849              
1850             =cut
1851              
1852             sub read_procedure_sql
1853             {
1854 0     0 1   my $self = shift;
1855 0           my($name) = @_;
1856              
1857             # File slurp mode.
1858 0           local $/;
1859              
1860 0           open my $fh, "$self->{dir}/procedure/$name.sql";
1861 0           my $sql = <$fh>;
1862 0           close $fh;
1863              
1864 0           return $sql;
1865             }
1866              
1867             =item $o->queue_push_procedure ( $name )
1868              
1869             =cut
1870              
1871             sub queue_push_procedure : method
1872             {
1873 0     0 1   my $self = shift;
1874 0           my( $name ) = @_;
1875              
1876 0           my $new_sql = $self->read_procedure_sql( $name );
1877              
1878 0           my($current_sql);
1879 0           eval {
1880 0           $current_sql = $self->pull_procedure_sql( $name );
1881             };
1882              
1883 0 0         if( $current_sql ) {
1884 0 0         if( $new_sql ne $current_sql ) {
1885 0           $self->queue_drop_procedure( $name, $new_sql );
1886 0           $self->queue_create_procedure( $name, $new_sql );
1887             }
1888             } else {
1889 0           $self->queue_create_procedure( $name, $new_sql );
1890             }
1891             }
1892              
1893             =item $o->queue_drop_procedure ( $name )
1894              
1895             =cut
1896              
1897             sub queue_drop_procedure : method
1898             {
1899 0     0 1   my $self = shift;
1900 0           my($name) = @_;
1901              
1902 0           $self->__queue_sql( 'drop_procedure',
1903             "Drop procedure $name\n",
1904             "DROP PROCEDURE `$name`",
1905             );
1906             }
1907              
1908             =item $o->queue_create_procedure ( $name, $sql )
1909              
1910             =cut
1911              
1912             sub queue_create_procedure : method
1913             {
1914 0     0 1   my $self = shift;
1915 0           my( $name, $sql ) = @_;
1916              
1917 0           $self->__queue_sql( 'create_procedure',
1918             "Create procedure $name\n",
1919             $sql,
1920             );
1921             }
1922              
1923             =item $o->queue_push_procedures ()
1924              
1925             =cut
1926              
1927             sub queue_push_procedures
1928             {
1929 0     0 1   my $self = shift;
1930              
1931 0           my @procedures = $self->procedure_names;
1932 0           for my $procedure ( @procedures ) {
1933 0           $self->queue_push_procedure( $procedure );
1934             }
1935              
1936 0 0         if( $self->{remove}{procedure} ) {
1937 0           for my $name ( $self->query_procedure_names ) {
1938 0 0         next if grep { $_ eq $name } @procedures;
  0            
1939 0           $self->queue_drop_procedure( $name );
1940             }
1941             }
1942             }
1943              
1944             =item $o->query_procedure_names ()
1945              
1946             =cut
1947              
1948             sub query_procedure_names : method
1949             {
1950 0     0 1   my $self = shift;
1951 0           my $dbh = $self->{dbh};
1952              
1953 0           my $sth = $dbh->prepare( 'SHOW PROCEDURE STATUS WHERE Db=?' );
1954 0           $sth->execute($self->{connect}{schema});
1955              
1956 0           my @names = ();
1957 0           while( my $procedure = $sth->fetchrow_hashref() ) {
1958 0           push @names, $procedure->{Name};
1959             }
1960              
1961 0           return @names;
1962             }
1963              
1964             =item $o->remove_procedure_sql( $name )
1965              
1966             =cut
1967              
1968             sub remove_procedure_sql : method
1969             {
1970 0     0 1   my $self = shift;
1971 0           my( $name ) = @_;
1972 0           unlink "$self->{dir}/procedure/$name.sql";
1973             }
1974              
1975             =item $o->pull_procedure_sql ( $name )
1976              
1977             =cut
1978              
1979             sub pull_procedure_sql : method
1980             {
1981 0     0 1   my $self = shift;
1982 0           my($name) = @_;
1983 0           my $dbh = $self->{dbh};
1984              
1985 0           my $desc_sth = $dbh->prepare( "SHOW CREATE PROCEDURE `$name`" );
1986 0           $desc_sth->execute();
1987 0           my $desc = $desc_sth->fetchrow_hashref();
1988 0           my $sql = $desc->{'Create Procedure'};
1989 0 0         $sql .= "\n" unless $sql =~ m/\n$/;
1990 0           return $sql;
1991             }
1992              
1993             =item $o->pull_procedures ()
1994              
1995             =cut
1996              
1997             sub pull_procedures : method
1998             {
1999 0     0 1   my $self = shift;
2000              
2001             # Keep track of procedure names found on the database to support
2002             # remove feature.
2003 0           my %found_procedure;
2004              
2005 0           for my $name ( $self->query_procedure_names ) {
2006 0           $found_procedure{$name} = 1;
2007              
2008 0           my $sql = $self->pull_procedure_sql( $name );
2009              
2010 0           $self->write_procedure_sql( $name, $sql );
2011             }
2012              
2013 0 0         if( $self->{remove}{procedure} ) {
2014 0           for my $procedure ( $self->procedure_names ) {
2015 0 0         next if $found_procedure{ $procedure };
2016 0           $self->remove_procedure_sql( $procedure );
2017             }
2018             }
2019             }
2020              
2021             =item $o->write_procedure_sql( $name, $sql )
2022              
2023             =cut
2024              
2025             sub write_procedure_sql : method
2026             {
2027 0     0 1   my $self = shift;
2028 0           my($name, $sql) = @_;
2029 0           my $fh;
2030              
2031             # Make table directory if required.
2032 0 0         mkdir "$self->{dir}/procedure"
2033             unless -d "$self->{dir}/procedure";
2034              
2035 0           open $fh, ">", "$self->{dir}/procedure/$name.sql";
2036 0           print $fh $sql;
2037 0           close $fh;
2038             }
2039              
2040             ### GENERAL ###
2041              
2042             =item $o->pull ()
2043              
2044             Handle the pull command.
2045              
2046             =cut
2047              
2048             sub pull : method
2049             {
2050 0     0 1   my $self = shift;
2051              
2052 0 0 0       $self->__dbi_connect() unless $self->{dbh} and $self->{dbh}->ping;
2053              
2054 0           $self->pull_table_definitions();
2055 0           $self->pull_view_definitions();
2056 0           $self->pull_trigger_fragments();
2057 0           $self->pull_procedures();
2058             }
2059              
2060             =item $o->push ()
2061              
2062             Handle the push command.
2063              
2064             =cut
2065              
2066             sub push : method
2067             {
2068 0     0 1   my $self = shift;
2069              
2070 0 0 0       $self->__dbi_connect() unless $self->{dbh} and $self->{dbh}->ping;
2071              
2072 0           $self->queue_push_table_definitions();
2073 0           $self->queue_push_view_definitions();
2074 0           $self->queue_push_trigger_definitions();
2075 0           $self->queue_push_procedures();
2076              
2077 0           $self->run_queue();
2078             }
2079              
2080             =item $o->run_queue()
2081              
2082             Process any actions in todo queue. Returns number of actions executed.
2083              
2084             =cut
2085              
2086             sub run_queue : method
2087             {
2088 0     0 1   my $self = shift;
2089              
2090 0           my $count = 0;
2091 0           for my $action ( TODO_ACTIONS ) {
2092 0           while( @{ $self->{todo}{$action} } ) {
  0            
2093 0           my $task = shift @{ $self->{todo}{$action} };
  0            
2094 0           ++$count;
2095 0 0         print $task->{desc}, "\n" unless $QUIET;
2096 0 0 0       print "\n$task->{sql}\n\n" if $VERBOSE or $DRYRUN;
2097 0           eval {
2098 0 0         $self->{dbh}->do( $task->{sql} ) unless $DRYRUN;
2099             };
2100 0 0         die "Error executing SQL: $@\n$task->{sql}\n" if $@;
2101             }
2102             }
2103              
2104 0           return $count;
2105             }
2106              
2107             =item $o->make_archive ()
2108              
2109             Handle the make-archive command.
2110              
2111             =cut
2112              
2113             sub make_archive : method
2114             {
2115 0     0 1   my $self = shift;
2116              
2117             # Detect tables by presence of revision column if option wasn't provided on
2118             # the command-line.
2119 0           my @tables_desc;
2120 0 0         if( @{ $self->{tables} } ) {
  0            
2121             # Get table information for all tables for which we will create/update
2122             # archive tables.
2123 0           @tables_desc = map { $self->get_table_desc( $_ ) } @{ $self->{tables} };
  0            
  0            
2124             } else {
2125 0           @tables_desc = $self->find_data_tables_with_revision( );
2126             }
2127              
2128             # Check that all source tables have required columns.
2129 0           for my $table ( @tables_desc ) {
2130 0           $self->check_table_is_archive_capable( $table );
2131             }
2132              
2133             # Basic checks done, we should be good to go to start making and updating
2134             # tables.
2135 0           for my $table ( @tables_desc ) {
2136              
2137             # Make archive table description from source data table
2138 0           my $archive_table = $self->make_archive_table_desc( $table );
2139              
2140             # Check if there is a current archive table.
2141 0           my $current_archive_table;
2142 0           eval { $current_archive_table = $self->get_table_desc(
2143             $archive_table->{name}
2144 0           ) };
2145              
2146 0 0         if( $current_archive_table ) {
2147             # Check if any updates are required.
2148 0 0         print "Archive table `$current_archive_table->{name}` " .
2149             "found for `$table->{name}`.\n" if $VERBOSE;
2150              
2151             # Verify that the current archive table could be updated to new
2152             # requirements.
2153 0           $self->check_table_updatable(
2154             $current_archive_table,
2155             $archive_table
2156             );
2157              
2158             # Update the archive table definition.
2159 0           $self->write_table_definition( $archive_table );
2160             } else {
2161 0 0         print "Writing archive table `$archive_table->{name}` " .
2162             "definition for `$table->{name}`.\n" if $VERBOSE;
2163 0           $self->write_table_definition( $archive_table );
2164             }
2165              
2166 0           $self->write_archive_trigger_fragments( $table, $archive_table );
2167             }
2168             }
2169              
2170             =item $o->set_dbh ()
2171              
2172             Explicitly set the database handle.
2173              
2174             =cut
2175              
2176             sub set_dbh : method
2177             {
2178 0     0 1   my $self = shift;
2179 0           my($dbh) = @_;
2180 0           $self->{dbh} = $dbh;
2181             }
2182              
2183             =back
2184              
2185             =head1 AUTHOR
2186              
2187             Johnathan Kupferer
2188              
2189             =head1 COPYRIGHT
2190              
2191             Copyright (C) 2015 The University of Illinois at Chicago. All Rights Reserved.
2192              
2193             This module is free software; you can redistribute it and/or modify it
2194             under the same terms as Perl itself.
2195              
2196             =cut
2197              
2198             1;