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   217303 use strict;
  11         48  
  11         325  
6 11     11   59 use warnings;
  11         23  
  11         312  
7              
8 11     11   7686 use Archive::Zip qw( :ERROR_CODES :CONSTANTS );
  11         924814  
  11         1852  
9 11     11   123 use Carp;
  11         33  
  11         662  
10 11     11   6663 use List::MoreUtils qw(all);
  11         145151  
  11         74  
11 11     11   18498 use Moo;
  11         129442  
  11         62  
12 11     11   17055 use Scalar::Util qw(blessed);
  11         28  
  11         571  
13 11     11   7751 use XML::LibXML;
  11         443142  
  11         95  
14 11     11   8180 use YAML::Tiny;
  11         65766  
  11         693  
15              
16 11     11   5726 use MySQL::Workbench::Parser::Table;
  11         44  
  11         431  
17 11     11   5797 use MySQL::Workbench::Parser::View;
  11         78  
  11         18598  
18              
19             our $VERSION = '1.10';
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 62 my $self = shift;
68              
69 6         110 my $tables = $self->tables;
70 6         68 my %info;
71 6         18 for my $table ( @{$tables} ) {
  6         21  
72 13         23 push @{$info{tables}}, $table->as_hash;
  13         64  
73             }
74              
75 6         14 for my $view ( @{ $self->views } ) {
  6         119  
76 2         12 push @{$info{views}}, $view->as_hash;
  2         9  
77             }
78              
79 6         119 my $yaml = YAML::Tiny->new;
80 6         72 $yaml->[0] = \%info;
81              
82 6         46 return $yaml->write_string;
83             }
84              
85             sub get_datatype {
86 69     69 1 123 my $self = shift;
87              
88 69         1676 my $datatypes = $self->datatypes;
89 69         736 return $datatypes->{$_[0]};
90             }
91              
92             sub _parse_tables {
93 9     9   1432 my ($self) = shift;
94              
95 9         79 $self->_parse;
96 8         3119 $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   53 my $self = shift;
108              
109 10         110 my $zip = Archive::Zip->new;
110 10 100       574 if ( $zip->read( $self->file ) != AZ_OK ) {
111 1         3076 croak "can't read file " . $self->file;
112             }
113              
114 9         16343 my $xml = $zip->contents( 'document.mwb.xml' );
115 9         18070 my $dom = XML::LibXML->load_xml( string => $xml );
116              
117 9         34556 $self->_set_dom( $dom );
118              
119 9         74 my %datatypes;
120 9         156 my @simple_type_nodes = $dom->documentElement->findnodes( './/value[@key="simpleDatatypes"]/link' );
121 9         7918 for my $type_node ( @simple_type_nodes ) {
122 390         1995 my $link = $type_node->textContent;
123 390         1537 my $datatype = uc +(split /\./, $link)[-1];
124 390         868 $datatype =~ s/_F\z//;
125              
126 390         1352 $datatypes{$link} = { name => $datatype, length => undef };
127             }
128              
129 9         132 my @user_type_structs = $dom->documentElement->findnodes( './/value[@key="userDatatypes"]' );
130 9         6922 for my $type_structs ( @user_type_structs ) {
131 9         171 my @user_types = $type_structs->findnodes( './value[@struct-name="db.UserDatatype"]' );
132 9         564 for my $type ( @user_types ) {
133 166         461 my $name = $type->findvalue( '@id' );
134 166         10264 my $sql = $type->findvalue( './value[@key="sqlDefinition"]' );
135 166         11395 my ($orig) = $sql =~ m{^([A-Z]+)};
136 166         646 my ($length) = $sql =~ m{\( (\d+) \)}x;
137 166         386 my ($precision) = $sql =~ m{\( (\d+,\d+) \)}x;
138 166         503 my ($args) = $sql =~ m{\( (.+?) \)}x;
139 166         421 my $gui_name = $type->findvalue( './value[@key="name"]' );
140              
141 166         12240 $datatypes{$name} = { name => $orig, length => $length, precision => $precision, gui_name => $gui_name, args => $args };
142             }
143             }
144              
145 9         1483 $self->_set_datatypes( \%datatypes );
146              
147 9         103 my @tables;
148              
149 9         81 my @table_nodes = $dom->documentElement->findnodes( './/value[@struct-name="db.mysql.Table"]' );
150 9         6893 for my $table_node ( @table_nodes ) {
151 27         912 push @tables, MySQL::Workbench::Parser::Table->new(
152             node => $table_node,
153             parser => $self,
154             );
155             }
156              
157 9 100       281 $self->_lint( \@tables ) if $self->lint;
158 9         287 $self->_set_tables( \@tables );
159              
160 9         119 my @views;
161              
162 9         90 my @view_nodes = $dom->documentElement->findnodes( './/value[@struct-name="db.mysql.View"]' );
163              
164 9         8192 my %column_mapping;
165 9 100       175 if ( @view_nodes ) {
166              
167             TABLE:
168 1         3 for my $table ( @tables ) {
169 2         7 my $name = $table->name;
170            
171 2         2 for my $col ( @{ $table->columns } ) {
  2         42  
172 4         21 my $col_name = $col->name;
173 4         17 $column_mapping{$name}->{$col_name} = $col;
174             }
175             }
176             }
177              
178 9         41 for my $view_node ( @view_nodes ) {
179 2         37 push @views, MySQL::Workbench::Parser::View->new(
180             node => $view_node,
181             column_mapping => \%column_mapping,
182             parser => $self,
183             );
184             }
185              
186 9         265 $self->_set_views( \@views );
187             }
188              
189             sub _lint {
190 10     10   744 my ($self, $tables) = @_;
191              
192 10 100       52 return if !ref $tables;
193 9 100       43 return if 'ARRAY' ne ref $tables;
194              
195 8         33 my %tablenames;
196             my %indexes;
197 8         0 my %duplicate_columns;
198              
199 8         18 for my $table ( @{ $tables } ) {
  8         23  
200 21         62 my $name = $table->name;
201              
202 21         57 $tablenames{$name}++;
203              
204             INDEX:
205 21         38 for my $index ( @{ $table->indexes } ) {
  21         369  
206 40         242 my $index_name = $index->name;
207              
208 40 100       116 next INDEX if $index_name eq 'PRIMARY';
209 19 100       79 next INDEX if $index->type eq 'UNIQUE';
210              
211 10         35 $indexes{$index_name}++;
212             }
213              
214 21         42 my %columns;
215              
216             COLUMN:
217 21         34 for my $column ( @{ $table->columns } ) {
  21         335  
218 54         246 my $column_name = $column->name;
219 54 100       134 $duplicate_columns{$name}++ if $columns{$column_name};
220 54         154 $columns{$column_name}++;
221             }
222             }
223              
224             # warn if table names occur more than once
225 8         59 my @duplicate_tables = grep{ $tablenames{$_} > 1 }sort keys %tablenames;
  20         62  
226 8 100       40 if ( @duplicate_tables ) {
227 1         246 carp 'duplicate table names (' .
228             ( join ', ', @duplicate_tables ).
229             ')';
230             }
231              
232             # warn if index name occurs more than once
233 8         179 my @duplicate_indexes = grep{ $indexes{$_} > 1 }sort keys %indexes;
  9         31  
234 8 100       33 if ( @duplicate_indexes ) {
235 1         95 carp 'duplicate indexes (' .
236             ( join ', ', @duplicate_indexes ) .
237             ')';
238             }
239              
240             # warn if there are duplicate column names
241 8 100       73 if ( %duplicate_columns ) {
242 1         91 carp 'duplicate column names in a table (' .
243             ( join ', ', sort keys %duplicate_columns ).
244             ')';
245             }
246              
247 8         68 return 1;
248             }
249              
250             1;
251              
252             __END__