File Coverage

blib/lib/DBIx/Admin/BackupRestore.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             package DBIx::Admin::BackupRestore;
2              
3             # Documentation:
4             # POD-style documentation is at the end. Extract it with pod2html.*.
5             #
6             # Reference:
7             # Object Oriented Perl
8             # Damian Conway
9             # Manning
10             # 1-884777-79-1
11             # P 114
12             #
13             # Note:
14             # o Tab = 4 spaces || die.
15             #
16             # Author:
17             # Ron Savage
18             # Home page: http://savage.net.au/index.html
19             #
20             # Licence:
21             # Australian copyright (c) 2003 Ron Savage.
22             #
23             # All Programs of mine are 'OSI Certified Open Source Software';
24             # you can redistribute them and/or modify them under the terms of
25             # The Artistic License, a copy of which is available at:
26             # http://www.opensource.org/licenses/index.html
27              
28 1     1   22060 use strict;
  1         3  
  1         37  
29 1     1   5 use warnings;
  1         2  
  1         26  
30              
31 1     1   5 use Carp;
  1         2  
  1         122  
32 1     1   5 use File::Spec;
  1         3  
  1         34  
33 1     1   4472 use XML::Records;
  0            
  0            
34              
35             require 5.005_62;
36              
37             require Exporter;
38              
39             our @ISA = qw(Exporter);
40              
41             # Items to export into callers namespace by default. Note: do not export
42             # names by default without a very good reason. Use EXPORT_OK instead.
43             # Do not simply export all your public functions/methods/constants.
44              
45             # This allows declaration use DBIx::Admin::BackupRestore ':all';
46             # If you do not need this, moving things directly into @EXPORT or @EXPORT_OK
47             # will save memory.
48             our %EXPORT_TAGS = ( 'all' => [ qw(
49              
50             ) ] );
51              
52             our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
53              
54             our @EXPORT = qw(
55              
56             );
57             our $VERSION = '1.17';
58              
59             my(%_decode_xml) =
60             (
61             '&' => '&',
62             '<' => '<',
63             '>' => '>',
64             '"' => '"',
65             );
66              
67             my(%_encode_xml) =
68             (
69             '&' => '&',
70             '<' => '<',
71             '>' => '>',
72             '"' => '"',
73             );
74              
75             # -----------------------------------------------
76              
77             # Preloaded methods go here.
78              
79             # -----------------------------------------------
80              
81             # Encapsulated class data.
82              
83             {
84             my(%_attr_data) =
85             (
86             _clean => 0,
87             _croak_on_error => 1,
88             _dbh => '',
89             _dbi_catalog => undef,
90             _dbi_schema => undef,
91             _dbi_table => '%',
92             _dbi_type => 'TABLE',
93             _fiddle_timestamp => 1,
94             _odbc => 0,
95             _output_dir_name => '',
96             _rename_columns => {},
97             _rename_tables => {},
98             _skip_schema => [],
99             _skip_tables => [],
100             _transform_tablenames => 0,
101             _verbose => 0,
102             );
103              
104             sub _default_for
105             {
106             my($self, $attr_name) = @_;
107              
108             $_attr_data{$attr_name};
109             }
110              
111             sub _standard_keys
112             {
113             keys %_attr_data;
114             }
115              
116             } # End of encapsulated class data.
117              
118             # -----------------------------------------------
119              
120             sub adjust_case
121             {
122             my($self, $s) = @_;
123              
124             $$self{'_dbh'}{'FetchHashKeyName'} eq 'NAME_uc' ? uc $s : $$self{'_dbh'}{'FetchHashKeyName'} eq 'NAME_lc' ? lc $s : $s;
125              
126             } # End of adjust_case.
127              
128             # -----------------------------------------------
129              
130             sub backup
131             {
132             my($self, $database) = @_;
133              
134             Carp::croak('Missing parameter to new(): dbh') if (! $$self{'_dbh'});
135              
136             $$self{'_quote'} = $$self{'_dbh'} ? $$self{'_dbh'} -> get_info(29) : ''; # SQL_IDENTIFIER_QUOTE_CHAR.
137             $$self{'_tables'} = $$self{'_odbc'} ? $self -> odbc_tables() : $self -> tables();
138             $$self{'_xml'} = qq|\n|;
139             $$self{'_xml'} .= qq|\n|;
140              
141             my($column_name);
142             my($data, $display_sql, $display_table);
143             my($field);
144             my($i);
145             my($output_column_name);
146             my($sql, $sth);
147             my($table_name);
148             my($xml);
149              
150             for $table_name (@{$$self{'_tables'} })
151             {
152             $self -> process_table('backup', $table_name);
153              
154             next if ($$self{'_skipping'});
155              
156             $display_table = $self -> adjust_case($$self{'_current_table'});
157             $sql = "select * from $$self{'_current_table'}";
158             $display_table = $$self{'_rename_tables'}{$display_table} ? $$self{'_rename_tables'}{$display_table} : $display_table;
159             $display_sql = "select * from $display_table";
160             $display_sql = $self -> adjust_case($display_sql);
161             $display_sql = $self -> encode_xml($display_sql);
162             $$self{'_xml'} .= qq|\t\n|;
163             $sth = $$self{'_dbh'} -> prepare($sql) || Carp::croak("Can't prepare($sql): $DBI::errstr");
164              
165             eval{$sth -> execute()};
166              
167             if ($@)
168             {
169             Carp::croak("Can't execute($sql): $DBI::errstr") if ($$self{'_croak_on_error'});
170              
171             print STDERR "$@" if ($$self{'_verbose'});
172              
173             next;
174             }
175              
176             $column_name = $$sth{$$self{'_dbh'}{'FetchHashKeyName'} };
177             $$self{'_column_name'}{$display_table} = [map{$i = $$self{'_rename_columns'}{$_} ? $$self{'_rename_columns'}{$_} : $_; $i =~ tr/ /_/; $i} sort @$column_name];
178              
179             while ($data = $sth -> fetch() )
180             {
181             $i = - 1;
182             $xml = '';
183              
184             for $field (@$data)
185             {
186             $i++;
187              
188             if (defined($field) )
189             {
190             $field =~ tr/\x20-\x7E//cd if ($$self{'_clean'});
191             $output_column_name = $$self{'_rename_columns'}{$$column_name[$i]} ? $$self{'_rename_columns'}{$$column_name[$i]} : $$column_name[$i];
192             $output_column_name =~ tr/ /_/;
193             $xml .= "\t\t\t<$output_column_name>" . $self -> encode_xml($field) . '\n";
194             }
195             }
196              
197             $$self{'_xml'} .= "\t\t\n$xml\t\t\n" if ($xml);
198             }
199              
200             Carp::croak("Can't fetchrow_hashref($sql): $DBI::errstr") if ($DBI::errstr);
201              
202             $$self{'_xml'} .= "\t\n";
203             }
204              
205             $$self{'_xml'} .= "\n";
206              
207             } # End of backup.
208              
209             # -----------------------------------------------
210              
211             sub decode_xml
212             {
213             my($self, $s) = @_;
214              
215             for my $key (keys %_decode_xml)
216             {
217             $s =~ s/$key/$_decode_xml{$key}/eg;
218             }
219              
220             $s;
221              
222             } # End of decode_xml.
223              
224             # -----------------------------------------------
225              
226             sub encode_xml
227             {
228             my($self, $str) = @_;
229             $str =~ s/([&<>"])/$_encode_xml{$1}/eg;
230              
231             $str;
232              
233             } # End of encode_xml.
234              
235             # -----------------------------------------------
236              
237             sub get_column_names
238             {
239             my($self) = @_;
240              
241             $$self{'_column_name'};
242              
243             } # End of get_column_names.
244              
245             # -----------------------------------------------
246              
247             sub new
248             {
249             my($class, %arg) = @_;
250             my($self) = bless({}, $class);
251              
252             for my $attr_name ($self -> _standard_keys() )
253             {
254             my($arg_name) = $attr_name =~ /^_(.*)/;
255              
256             if (exists($arg{$arg_name}) )
257             {
258             $$self{$attr_name} = $arg{$arg_name};
259             }
260             else
261             {
262             $$self{$attr_name} = $self -> _default_for($attr_name);
263             }
264             }
265              
266             $$self{'_column_name'} = {};
267             $$self{'_current_schema'} = '';
268             $$self{'_current_table'} = '';
269             $$self{'_database'} = [];
270             $$self{'_key'} = [];
271             $$self{'_output_is_open'} = 0;
272             $$self{'_quote'} = '';
273             $$self{'_restored'} = {};
274             $$self{'_skipped'} = {};
275             $$self{'_skipping'} = 0;
276             @{$$self{'_skip_schema_name'} }{@{$$self{'_skip_schema'} } } = (1) x @{$$self{'_skip_schema'} };
277             @{$$self{'_skip_table_name'} }{@{$$self{'_skip_tables'} } } = (1) x @{$$self{'_skip_tables'} };
278             $$self{'_value'} = [];
279             $$self{'_xml'} = '';
280              
281             return $self;
282              
283             } # End of new.
284              
285             # -----------------------------------------------
286              
287             sub odbc_tables
288             {
289             my($self) = @_;
290              
291             [
292             map{s/^$$self{'_quote'}.+?$$self{'_quote'}\.$$self{'_quote'}(.+)$$self{'_quote'}/$1/; $_}
293             grep{! /^BIN\$.+\$./} # Discard 'funny' Oracle table names, like BIN$C544WGedCuHgRAADuk1i5g==$0.
294             sort $$self{'_dbh'} -> tables()
295             ];
296              
297             } # End of odbc_tables.
298              
299             # -----------------------------------------------
300              
301             sub process_table
302             {
303             my($self, $action, $table_name) = @_;
304             $$self{'_current_table'} = $self -> decode_xml($table_name);
305              
306             if ( ($$self{'_transform_tablenames'} == 1) && ($$self{'_current_table'} =~ /^(.+?)\.(.+)$/) )
307             {
308             $$self{'_current_schema'} = $1;
309             $$self{'_current_table'} = $2;
310             }
311              
312             if ($$self{'_skip_schema_name'}{$$self{'_current_schema'} } || $$self{'_skip_table_name'}{$$self{'_current_table'} })
313             {
314             # With restore_in_order we read the input file N times,
315             # but we don't want to _report_ the same table N times.
316             # Hence the hash $$self{'_skipped'}.
317              
318             print STDERR "Skip table: $$self{'_current_table'}. \n" if ($$self{'_verbose'} && ! $$self{'_skipped'}{$$self{'_current_table'} });
319              
320             $$self{'_skipping'} = 1;
321             $$self{'_skipped'}{$$self{'_current_table'} } = 1;
322             }
323             else
324             {
325             # With restore_in_order we read the input file N times,
326             # but we don't want to _report_ or _restore_ the same table N times.
327             # Hence the hash $$self{'_restored'}.
328              
329             print STDERR "$action table: $$self{'_current_table'}. \n" if ($$self{'_verbose'} && ! $$self{'_restored'}{$$self{'_current_table'} });
330              
331             $$self{'_skipping'} = 0;
332             $$self{'_restored'}{$$self{'_current_table'} } = 1;
333             }
334              
335             } # End of process_table.
336              
337             # -----------------------------------------------
338              
339             sub restore
340             {
341             my($self, $file_name) = @_;
342              
343             Carp::croak('Missing parameter to new(): dbh') if (! $$self{'_dbh'});
344              
345             open(INX, $file_name) || Carp::croak("Can't open($file_name): $!");
346              
347             my($line);
348              
349             while ($line = )
350             {
351             next if ($line =~ m!^(<\?xml|
352              
353             if ($line =~ m!!i)
354             {
355             $self -> process_table('Restore', $1);
356             }
357             elsif ( (! $$self{'_skipping'}) && ($line =~ m!!i) )
358             {
359             # There may be a different number of fields from one row to the next.
360             # Remember, only non-null fields are output by method backup().
361              
362             $$self{'_key'} = [];
363             $$self{'_value'} = [];
364              
365             while ( ($line = ) !~ m!!i)
366             {
367             if ($line =~ m!^\s*<(.+?)>(.*?)!i)
368             {
369             push @{$$self{'_key'} }, $1;
370              
371             $self -> transform($1, $self -> decode_xml($2) );
372             }
373             }
374              
375             $self -> write_row();
376             }
377             }
378              
379             close INX;
380              
381             [sort keys %{$$self{'_restored'} }];
382              
383             } # End of restore.
384              
385             # -----------------------------------------------
386              
387             sub restore_in_order
388             {
389             my($self, $input_file_name, $table) = @_;
390              
391             Carp::croak('Missing parameter to new(): dbh') if (! $$self{'_dbh'});
392              
393             my($table_name, $parser, $type, $record, $candidate_table, $row);
394              
395             for $table_name (@$table)
396             {
397             $parser = XML::Records -> new($input_file_name);
398              
399             $parser -> set_records('resultset');
400              
401             for (;;)
402             {
403             ($type, $record) = $parser -> get_record();
404              
405             # Exit if no data found.
406              
407             last if (! $record);
408              
409             $candidate_table = $1 if ($$record{'statement'} =~ m!select \* from (.+)!);
410              
411             # Skip if the data is not for the 'current' table.
412              
413             next if ($candidate_table ne $table_name);
414              
415             # Skip if the data is not wanted.
416              
417             next if ($$self{'_skipping'});
418              
419             $self -> process_table('Restore', $candidate_table);
420              
421             # Warning: At this point, if the input file has no data for a table,
422             # $$record{'row'} will be undef, so don't access @{$$record{'row'} }.
423              
424             next if (! $$record{'row'});
425              
426             # Warning. If the XML file contains 1 'record', XML::Records
427             # returns text or a hash ref, not an array ref containing one element.
428             # Due to the nature of our data, we can ignore the case of textual data.
429              
430             $$record{'row'} = [$$record{'row'}] if (ref $$record{'row'} ne 'ARRAY');
431              
432             for $row (@{$$record{'row'} })
433             {
434             # There may be a different number of fields from one row to the next.
435             # Remember, only non-null fields are output by method backup().
436              
437             @{$$self{'_key'} } = keys %$row;
438             $$self{'_value'} = [];
439              
440             $self -> transform($_, $$row{$_}) for @{$$self{'_key'} };
441             $self -> write_row();
442             }
443              
444             # Exit if table restored.
445              
446             last;
447             }
448             }
449              
450             } # End of restore_in_order.
451              
452             # -----------------------------------------------
453              
454             sub split
455             {
456             my($self, $file_name) = @_;
457              
458             open(INX, $file_name) || Carp::croak("Can't open($file_name): $!");
459              
460             my($line, $table_name, $output_file_name);
461              
462             while ($line = )
463             {
464             next if ($line =~ m!^(<\?xml|
465              
466             if ($line =~ m!^!i)
467             {
468             $$self{'_database'} = $1;
469              
470             next;
471             }
472              
473             if ($line =~ m!!i)
474             {
475             $table_name = $1;
476              
477             $self -> process_table('Split', $table_name);
478              
479             # Close off the previous output file, if any.
480              
481             if ($$self{'_output_is_open'})
482             {
483             $$self{'_output_is_open'} = 0;
484              
485             print OUT qq|\t\n|;
486             print OUT qq|\n|;
487              
488             close OUT;
489             }
490              
491             if (! $$self{'_skipping'})
492             {
493             # Start the next output file.
494              
495             $output_file_name = "$$self{'_current_table'}.xml";
496             $output_file_name = "$$self{'_current_schema'}.$output_file_name" if ($$self{'_current_schema'});
497             $output_file_name = File::Spec -> catdir($$self{'_output_dir_name'}, $output_file_name);
498             $$self{'_output_is_open'} = 1;
499              
500             open(OUT, "> $output_file_name") || Carp::croak("Can't open($output_file_name): $!");
501              
502             print OUT qq|\n|;
503             print OUT qq|\n|;
504             print OUT qq|\t\n|;
505             }
506             }
507             elsif ( (! $$self{'_skipping'}) && ($line =~ m!!i) )
508             {
509             # There may be a different number of fields from one row to the next.
510             # Remember, only non-null fields are output by method backup().
511              
512             print OUT qq|\t\t\n|;
513              
514             while ( ($line = ) !~ m!!i)
515             {
516             print OUT $line;
517             }
518              
519             print OUT qq|\t\t\n|;
520             }
521             }
522              
523             close INX;
524              
525             # Close off the previous file, if any.
526              
527             if ($$self{'_output_is_open'})
528             {
529             print OUT qq|\t\n|;
530             print OUT qq|\n|;
531              
532             close OUT;
533             }
534              
535             [sort keys %{$$self{'_restored'} }];
536              
537             } # End of split.
538              
539             # -----------------------------------------------
540              
541             sub tables
542             {
543             my($self) = @_;
544              
545             [
546             sort
547             map{s/$$self{'_quote'}//g; $_}
548             grep{! /^BIN\$.+\$./} # Discard 'funny' Oracle table names, like BIN$C544WGedCuHgRAADuk1i5g==$0.
549             map{$$_{'TABLE_NAME'} }
550             @{$$self{'_dbh'}
551             -> table_info($$self{'_dbi_catalog'}, $$self{'_dbi_schema'}, $$self{'_dbi_table'}, $$self{'_dbi_type'})
552             -> fetchall_arrayref({})}
553             ];
554              
555             } # End of tables.
556              
557             # -----------------------------------------------
558              
559             sub transform
560             {
561             my($self, $key, $value) = @_;
562              
563             if ($key =~ /timestamp/)
564             {
565             if ($$self{'_fiddle_timestamp'} & 0x01)
566             {
567             $value = '19700101' if ($value =~ /^0000/);
568             $value = substr($value, 0, 4) . '-' . substr($value, 4, 2) . '-' . substr($value, 6, 2) . ' 00:00:00';
569             }
570             elsif ($$self{'_fiddle_timestamp'} & 0x02)
571             {
572             $value = '1970-01-01 00:00:00' if ($value =~ /^0000/);
573             }
574              
575             if ($$self{'_fiddle_timestamp'} & 0x80)
576             {
577             $value = '1970-01-01 00:00:01' if ($value eq '1970-01-01 00:00:00');
578             }
579             }
580              
581             push @{$$self{'_value'} }, $value;
582              
583             } # End of transform.
584              
585             # -----------------------------------------------
586              
587             sub write_row
588             {
589             my($self) = @_;
590              
591             if ($$self{'_skip_schema_name'}{$$self{'_current_schema'} } || $$self{'_skip_table_name'}{$$self{'_current_table'} })
592             {
593             }
594             else
595             {
596             my($sql) = "insert into $$self{'_current_table'} (" . join(', ', @{$$self{'_key'} }) . ') values (' . join(', ', ('?') x @{$$self{'_key'} }) . ')';
597             my($sth) = $$self{'_dbh'} -> prepare($sql) || Carp::croak("Can't prepare($sql): $DBI::errstr");
598              
599             $sth -> execute(@{$$self{'_value'} }) || Carp::croak("Can't execute($sql): $DBI::errstr");
600             $sth -> finish();
601             }
602              
603             } # End of write_row.
604              
605             # -----------------------------------------------
606              
607             1;
608              
609             __END__