File Coverage

blib/lib/MySQL/Workbench/Parser.pm
Criterion Covered Total %
statement 131 134 97.7
branch 22 22 100.0
condition n/a
subroutine 16 17 94.1
pod 2 2 100.0
total 171 175 97.7


line stmt bran cond sub pod time code
1             package MySQL::Workbench::Parser;
2              
3             # ABSTRACT: parse .mwb files created with MySQL Workbench
4              
5 11     11   174802 use strict;
  11         40  
  11         283  
6 11     11   47 use warnings;
  11         19  
  11         282  
7              
8 11     11   6390 use Archive::Zip qw( :ERROR_CODES :CONSTANTS );
  11         780593  
  11         1421  
9 11     11   96 use Carp;
  11         22  
  11         581  
10 11     11   5390 use List::MoreUtils qw(all);
  11         122178  
  11         72  
11 11     11   15830 use Moo;
  11         111089  
  11         59  
12 11     11   14334 use Scalar::Util qw(blessed);
  11         26  
  11         488  
13 11     11   6685 use XML::LibXML;
  11         369249  
  11         83  
14 11     11   6991 use YAML::Tiny;
  11         54701  
  11         587  
15              
16 11     11   4884 use MySQL::Workbench::Parser::Table;
  11         42  
  11         381  
17 11     11   5257 use MySQL::Workbench::Parser::View;
  11         38  
  11         15106  
18              
19             our $VERSION = '1.09';
20              
21             has lint => ( is => 'ro', default => sub { 1 } );
22              
23             has file => (
24             is => 'ro',
25             required => 1,
26             isa => sub { -f $_[0] },
27             );
28              
29             has tables => (
30             is => 'rwp',
31             isa => sub {
32             ref $_[0] && ref $_[0] eq 'ARRAY' &&
33             all { blessed $_ && $_->isa( 'MySQL::Workbench::Parser::Table' ) }@{$_[0]} ;
34             },
35             lazy => 1,
36             builder => \&_parse_tables,
37             );
38              
39             has views => (
40             is => 'rwp',
41             isa => sub {
42             ref $_[0] && ref $_[0] eq 'ARRAY' &&
43             all { blessed $_ && $_->isa( 'MySQL::Workbench::Parser::View' ) }@{$_[0]} ;
44             },
45             lazy => 1,
46             builder => \&_parse_views,
47             );
48              
49             has datatypes => (
50             is => 'rwp',
51             isa => sub {
52             ref $_[0] && ref $_[0] eq 'HASH' &&
53             all { !ref $_[0]->{$_} }keys %{ $_[0] };
54             },
55             lazy => 1,
56             default => sub { +{} },
57             );
58              
59             has dom => (
60             is => 'rwp',
61             isa => sub {
62             blessed $_[0] && $_[0]->isa('XML::LibXML');
63             },
64             );
65              
66             sub dump {
67 6     6 1 51 my $self = shift;
68              
69 6         90 my $tables = $self->tables;
70 6         63 my %info;
71 6         11 for my $table ( @{$tables} ) {
  6         20  
72 13         28 push @{$info{tables}}, $table->as_hash;
  13         53  
73             }
74              
75 6         29 for my $view ( @{ $self->views } ) {
  6         117  
76 2         10 push @{$info{views}}, $view->as_hash;
  2         7  
77             }
78              
79 6         87 my $yaml = YAML::Tiny->new;
80 6         65 $yaml->[0] = \%info;
81              
82 6         37 return $yaml->write_string;
83             }
84              
85             sub get_datatype {
86 69     69 1 108 my $self = shift;
87              
88 69         1316 my $datatypes = $self->datatypes;
89 69         602 return $datatypes->{$_[0]};
90             }
91              
92             sub _parse_tables {
93 9     9   1173 my ($self) = shift;
94              
95 9         31 $self->_parse;
96 8         2676 $self->tables;
97             }
98              
99             sub _parse_views {
100 0     0   0 my ($self) = shift;
101              
102 0         0 $self->_parse;
103 0         0 $self->views;
104             }
105              
106             sub _parse {
107 10     10   32 my $self = shift;
108              
109 10         83 my $zip = Archive::Zip->new;
110 10 100       471 if ( $zip->read( $self->file ) != AZ_OK ) {
111 1         2495 croak "can't read file " . $self->file;
112             }
113              
114 9         13149 my $xml = $zip->contents( 'document.mwb.xml' );
115 9         14632 my $dom = XML::LibXML->load_xml( string => $xml );
116              
117 9         27658 $self->_set_dom( $dom );
118              
119 9         67 my %datatypes;
120 9         128 my @simple_type_nodes = $dom->documentElement->findnodes( './/value[@key="simpleDatatypes"]/link' );
121 9         6264 for my $type_node ( @simple_type_nodes ) {
122 390         1651 my $link = $type_node->textContent;
123 390         1295 my $datatype = uc +(split /\./, $link)[-1];
124 390         718 $datatype =~ s/_F\z//;
125              
126 390         1033 $datatypes{$link} = { name => $datatype, length => undef };
127             }
128              
129 9         90 my @user_type_structs = $dom->documentElement->findnodes( './/value[@key="userDatatypes"]' );
130 9         6022 for my $type_structs ( @user_type_structs ) {
131 9         153 my @user_types = $type_structs->findnodes( './value[@struct-name="db.UserDatatype"]' );
132 9         436 for my $type ( @user_types ) {
133 166         364 my $name = $type->findvalue( '@id' );
134 166         8116 my $sql = $type->findvalue( './value[@key="sqlDefinition"]' );
135 166         9471 my ($orig) = $sql =~ m{^([A-Z]+)};
136 166         525 my ($length) = $sql =~ m{\( (\d+) \)}x;
137 166         321 my ($precision) = $sql =~ m{\( (\d+,\d+) \)}x;
138 166         400 my ($args) = $sql =~ m{\( (.+?) \)}x;
139 166         331 my $gui_name = $type->findvalue( './value[@key="name"]' );
140              
141 166         9990 $datatypes{$name} = { name => $orig, length => $length, precision => $precision, gui_name => $gui_name, args => $args };
142             }
143             }
144              
145 9         1241 $self->_set_datatypes( \%datatypes );
146              
147 9         92 my @tables;
148              
149 9         64 my @table_nodes = $dom->documentElement->findnodes( './/value[@struct-name="db.mysql.Table"]' );
150 9         5739 for my $table_node ( @table_nodes ) {
151 27         798 push @tables, MySQL::Workbench::Parser::Table->new(
152             node => $table_node,
153             parser => $self,
154             );
155             }
156              
157 9 100       229 $self->_lint( \@tables ) if $self->lint;
158 9         170 $self->_set_tables( \@tables );
159              
160 9         86 my @views;
161              
162 9         82 my @view_nodes = $dom->documentElement->findnodes( './/value[@struct-name="db.mysql.View"]' );
163              
164 9         7098 my %column_mapping;
165 9 100       139 if ( @view_nodes ) {
166              
167             TABLE:
168 1         4 for my $table ( @tables ) {
169 2         6 my $name = $table->name;
170            
171 2         3 for my $col ( @{ $table->columns } ) {
  2         35  
172 4         16 my $col_name = $col->name;
173 4         12 $column_mapping{$name}->{$col_name} = $col;
174             }
175             }
176             }
177              
178 9         29 for my $view_node ( @view_nodes ) {
179 2         35 push @views, MySQL::Workbench::Parser::View->new(
180             node => $view_node,
181             column_mapping => \%column_mapping,
182             parser => $self,
183             );
184             }
185              
186 9         226 $self->_set_views( \@views );
187             }
188              
189             sub _lint {
190 10     10   615 my ($self, $tables) = @_;
191              
192 10 100       36 return if !ref $tables;
193 9 100       39 return if 'ARRAY' ne ref $tables;
194              
195 8         28 my %tablenames;
196             my %indexes;
197 8         0 my %duplicate_columns;
198              
199 8         14 for my $table ( @{ $tables } ) {
  8         26  
200 21         50 my $name = $table->name;
201              
202 21         54 $tablenames{$name}++;
203              
204             INDEX:
205 21         34 for my $index ( @{ $table->indexes } ) {
  21         299  
206 40         190 my $index_name = $index->name;
207              
208 40 100       111 next INDEX if $index_name eq 'PRIMARY';
209 19 100       47 next INDEX if $index->type eq 'UNIQUE';
210              
211 10         29 $indexes{$index_name}++;
212             }
213              
214 21         33 my %columns;
215              
216             COLUMN:
217 21         30 for my $column ( @{ $table->columns } ) {
  21         290  
218 54         199 my $column_name = $column->name;
219 54 100       107 $duplicate_columns{$name}++ if $columns{$column_name};
220 54         123 $columns{$column_name}++;
221             }
222             }
223              
224             # warn if table names occur more than once
225 8         67 my @duplicate_tables = grep{ $tablenames{$_} > 1 }sort keys %tablenames;
  20         55  
226 8 100       32 if ( @duplicate_tables ) {
227 1         183 carp 'duplicate table names (' .
228             ( join ', ', @duplicate_tables ).
229             ')';
230             }
231              
232             # warn if index name occurs more than once
233 8         134 my @duplicate_indexes = grep{ $indexes{$_} > 1 }sort keys %indexes;
  9         27  
234 8 100       28 if ( @duplicate_indexes ) {
235 1         78 carp 'duplicate indexes (' .
236             ( join ', ', @duplicate_indexes ) .
237             ')';
238             }
239              
240             # warn if there are duplicate column names
241 8 100       72 if ( %duplicate_columns ) {
242 1         71 carp 'duplicate column names in a table (' .
243             ( join ', ', sort keys %duplicate_columns ).
244             ')';
245             }
246              
247 8         56 return 1;
248             }
249              
250             1;
251              
252             __END__