File Coverage

blib/lib/MediaWiki/DumpFile/SQL.pm
Criterion Covered Total %
statement 151 175 86.2
branch 37 72 51.3
condition n/a
subroutine 27 30 90.0
pod 5 18 27.7
total 220 295 74.5


line stmt bran cond sub pod time code
1             package MediaWiki::DumpFile::SQL;
2              
3             our $VERSION = '0.2.2';
4              
5 7     7   33 use strict;
  7         12  
  7         202  
6 7     7   36 use warnings;
  7         12  
  7         167  
7 7     7   13952 use Data::Dumper;
  7         65381  
  7         605  
8 7     7   54 use Carp qw(croak);
  7         13  
  7         343  
9 7     7   38 use Scalar::Util qw(reftype);
  7         13  
  7         820  
10              
11 7     7   8796 use IO::Uncompress::AnyUncompress qw($AnyUncompressError);
  7         686618  
  7         14379  
12              
13             #public methods
14             sub new {
15 2     2 1 5 my ($class, $file) = @_;
16 2         4 my $self = { };
17            
18 2 50       9 if (! defined($file)) {
19 0         0 croak "must specify a filename or open filehandle";
20             }
21            
22 2         5 bless($self, $class);
23            
24 2         12 $self->{buffer} = [];
25 2         6 $self->{file} = $file;
26 2         3 $self->{fh} = undef;
27 2         5 $self->{table_name} = undef;
28 2         2 $self->{schema} = undef;
29 2         3 $self->{type_map} = undef;
30 2         3 $self->{table_statement} = undef;
31            
32 2         5 $self->create_type_map;
33 2         7 $self->open_file;
34 2         5 $self->parse_table;
35            
36 2         7 return $self;
37             }
38              
39             sub next {
40 94     94 1 48679 my ($self) = @_;
41 94         144 my $buffer = $self->{buffer};
42 94         99 my $next;
43            
44 94         260 while(! defined($next = shift(@$buffer))) {
45 4 100       16 if (! $self->parse_more) {
46 2         132 return undef;
47             }
48             }
49            
50 92         202 return $next;
51             }
52              
53             sub table_name {
54 2     2 1 9 my ($self) = @_;
55 2         12 return $self->{table_name};
56             }
57              
58             sub table_statement {
59 2     2 1 3 my ($self) = @_;
60 2         9 return $self->{table_statement};
61             }
62              
63             sub schema {
64 2     2 1 409 my ($self) = @_;
65 2         3 return @{$self->{schema}};
  2         8  
66             }
67              
68             #private methods
69             sub open_file {
70 2     2 0 3 my ($self) = @_;
71 2         3 my $file = $self->{file};
72 2         7 my $type = reftype($file);
73 2         1 my $fh;
74            
75 2         34 $self->{fh} = $fh = IO::Uncompress::AnyUncompress->new($file);
76 2         2438 my $line = <$fh>;
77            
78 2 50       230 if ($line !~ m/^-- MySQL dump/) {
79 0         0 die "expected MySQL dump file";
80             }
81            
82 2         5 return;
83             }
84              
85             sub parse_table {
86 2     2 0 3 my ($self) = @_;
87 2         3 my $fh = $self->{fh};
88 2         3 my $found = 0;
89 2         2 my $table;
90             my $table_statement;
91 0         0 my @cols;
92            
93             #find the CREATE TABLE line and get the table name
94 2         6 while(<$fh>) {
95 38 100       1578 if (m/^CREATE TABLE `([^`]+)` \(/) {
96 2         6 $table = $1;
97 2         3 $table_statement = $_;
98            
99 2         4 last;
100             }
101             }
102            
103 2 50       6 die "expected CREATE TABLE" unless defined($table);
104            
105 2         5 while(<$fh>) {
106 10         438 $table_statement .= $_;
107              
108 10 100       49 if (m/^\)/) {
    100          
109 2         4 last;
110             } elsif (m/^\s+`([^`]+)` (\w+)/) {
111             #this regex ^^^^ matches column names and types
112 4         28 push(@cols, [$1, $2]);
113             }
114             }
115            
116 2 50       5 if (! scalar(@cols)) {
117 0         0 die "Could not find columns for $table";
118             }
119              
120 2         4 $self->{table_name} = $table;
121 2         4 $self->{schema} = \@cols;
122 2         3 $self->{table_statement} = $table_statement;
123            
124 2         4 return 1;
125             }
126              
127             #returns false at EOF or true if more data was parsed
128             sub parse_more {
129 4     4 0 7 my ($self) = @_;
130 4         10 my $fh = $self->{fh};
131 4         5 my $insert;
132            
133 4 50       12 if (! defined($fh)) {
134 0         0 return 0;
135             }
136            
137 4         6 while(1) {
138 40         369 $insert = <$fh>;
139            
140 40 100       1835 if (! defined($insert)) {
141 2 50       22 close($fh) or die "could not close: $!";
142 2         102 $self->{fh} = undef;
143            
144 2         15 return 0;
145             }
146            
147 38 100       75 if ($insert =~ m/^INSERT INTO/) {
148 2         8 $self->parse($insert);
149 2         13 return 1;
150             }
151             }
152             }
153              
154             #this parses a complete INSERT line into the individual
155             #components
156             sub parse {
157 2     2 0 4 my ($self, $string) = @_;
158 2         4 my $buffer = $self->{buffer};
159 2         7 my $compiled = $self->compile_config;
160 2         4 my $found = 0;
161            
162 2         4 $_ = $string;
163            
164             #check the table name
165 2 50       20 m/^INSERT INTO `(.*?)` VALUES /g or die "expected header";
166 2 50       8 if ($self->{table_name} ne $1) {
167 0         0 die "table name mismatch: $1";
168             }
169            
170 2         3 while(1) {
171 92         81 my %new;
172 92         88 my $depth = 0;
173            
174             #apply the various regular expressions to the
175             #string in order
176 92         112 foreach my $handler (@$compiled) {
177 460         520 my ($col, $cb) = @$handler;
178 460         395 my $ret;
179            
180 460         408 $depth++;
181            
182             #these callbacks also use $_
183 460         412 eval { $ret = &$cb };
  460         722  
184            
185 460 50       697 if ($@) {
186 0         0 die "parse error pos:" . pos() . " depth:$depth error: $@";
187             }
188            
189             #column names starting with # are part of the parser, not user data
190 460 100       1265 if ($col !~ m/^#/) {
191 184         425 $new{$col} = $ret;
192             }
193             }
194            
195 92         150 push(@$buffer, \%new);
196 92         93 $found++;
197              
198 92 100       197 if (m/\G, ?/gc) {
    50          
199             #^^^^ match the delimiter between rows
200 90         117 next;
201             } elsif (m/\G;$/gc) {
202             #^^^^ match end of statement
203 2         4 last;
204             } else {
205 0         0 die "expected delimter or end of statement. pos:" . pos;
206             }
207             }
208            
209 2         19 return $found;
210             }
211              
212             #functions for the parsing engine
213              
214             #maps between MySQL types and our types
215             sub create_type_map {
216 2     2 0 3 my ($self) = @_;
217            
218 2         23 $self->{type_map} = {
219             int => 'int',
220             tinyint => 'int',
221             bigint => 'int',
222            
223             char => 'varchar',
224             varchar => 'varchar',
225             enum => 'varchar',
226            
227             double => 'float',
228            
229             timestamp => 'int',
230            
231             blob => 'varchar',
232             mediumblob => 'varchar',
233             mediumtext => 'varchar',
234             tinyblob => 'varchar',
235             varbinary => 'varchar',
236            
237             };
238            
239 2         3 return 1;
240             }
241              
242             #convert the schema into a list of callbacks
243             #that match the schema and extract data from it
244             sub compile_config {
245 2     2 0 3 my ($self) = @_;
246 2         3 my $schema = $self->{schema};
247 2         2 my @handlers;
248            
249 2         6 push(@handlers, ['#start', new_start_data()]);
250              
251 2         5 foreach (@$schema) {
252 4         18 my ($name, $type) = @$_;
253              
254 4         5 my $oldtype = $type;
255 4         11 $type = $self->{type_map}->{lc($type)};
256            
257 4 50       7 if (! defined($type)) {
258 0         0 die "type map failed for $oldtype";
259             }
260              
261 4 100       14 if ($type eq 'int') {
    50          
    0          
262 2         6 push(@handlers, [$name, new_int()], ['#delim', new_delim()]);
263             } elsif ($type eq 'varchar') {
264 2         7 push(@handlers, [$name, new_varchar()], ['#delim', new_delim()]);
265             } elsif($type eq 'float') {
266 0         0 push(@handlers, [$name, new_float()], ['#delim', new_delim()]);
267             } else {
268 0         0 die "unknown type: $type";
269             }
270             }
271            
272 2         4 pop(@handlers); #gets rid of that extra delimiter
273 2         9 push(@handlers, ['#end', new_end_data()]);
274            
275 2         5 return \@handlers;
276             }
277              
278             sub unescape {
279 0     0 0 0 my ($input) = @_;
280            
281 0 0       0 if ($input eq '\\\\') {
    0          
    0          
    0          
    0          
282 0         0 return '\\';
283             } elsif ($input eq "\\'") {
284 0         0 return("'");
285             } elsif ($input eq '\\"') {
286 0         0 return '"';
287             } elsif ($input eq '\\n') {
288 0         0 return "\n";
289             } elsif ($input eq '\\t') {
290 0         0 return "\t";
291             } else {
292 0         0 die "can not unescape $input";
293             }
294             }
295              
296              
297             #functions that create callbacks that match and extract
298             #data from INSERT lines
299              
300             #it is critical that these regular expressions use the /gc option
301             #or the parser will stop functioning as soon as a regex with
302             #out those options is encountered and debugging becomes
303             #almost impossible
304              
305             sub new_int {
306             return sub {
307 92 50   92   300 m/\GNULL/gc and return undef;
308 92 50       221 m/\G(-?[\d]+)/gc or die "expected int"; return $1;
  92         206  
309 2     2 0 12 };
310             }
311              
312             sub new_float {
313             return sub {
314 0 0   0   0 m/\GNULL/gc and return undef;
315 0 0       0 m/\G(-?[\d]+(?:\.[\d]+(e-?[\d]+)?)?)/gc or die "expected float"; return $1;
  0         0  
316             }
317 0     0 0 0 }
318              
319             sub new_varchar {
320             return sub {
321 92     92   73 my $data;
322            
323 92 50       148 m/\GNULL/gc and return undef;
324              
325             #does not handle very long strings; crashes perl 5.8.9 causes 5.10.1 to error out
326             #m/\G'((\\.|[^'])*)'/gc or die "expected varchar";
327             #thanks somni!
328 92 50       296 m/'((?:[^\\']*(?:\\.[^\\']*)*))'/gc or die "expected varchar";
329 92         119 $data = $1;
330 92         103 $data =~ s/(\\.)/unescape($1)/e;
  0         0  
331            
332 92         182 return $data;
333             }
334 2     2 0 10 }
335              
336             sub new_delim {
337             return sub {
338 92 50   92   213 m/\G, ?/gc or die "expected delimiter"; return undef;
  92         103  
339 4     4 0 20 };
340             }
341              
342             sub new_start_data {
343             return sub {
344 92 50   92   222 m/\G\(/gc or die "expected start of data set"; return undef;
  92         114  
345 2     2 0 11 };
346             }
347              
348             sub new_end_data {
349             return sub {
350 92 50   92   199 m/\G\)/gc or die "expected end of data set"; return undef;
  92         111  
351             }
352 2     2 0 7 }
353              
354             1;
355              
356             __END__