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   223988 use strict;
  11         50  
  11         360  
6 11     11   66 use warnings;
  11         22  
  11         400  
7              
8 11     11   7737 use Archive::Zip qw( :ERROR_CODES :CONSTANTS );
  11         954362  
  11         1737  
9 11     11   115 use Carp;
  11         32  
  11         677  
10 11     11   6534 use List::MoreUtils qw(all);
  11         153783  
  11         86  
11 11     11   28691 use Moo;
  11         142084  
  11         71  
12 11     11   17939 use Scalar::Util qw(blessed);
  11         32  
  11         591  
13 11     11   7914 use XML::LibXML;
  11         379623  
  11         98  
14 11     11   8826 use YAML::Tiny;
  11         67437  
  11         806  
15              
16 11     11   6311 use MySQL::Workbench::Parser::Table;
  11         46  
  11         441  
17 11     11   6094 use MySQL::Workbench::Parser::View;
  11         90  
  11         18773  
18              
19             our $VERSION = '1.11';
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 59 my $self = shift;
68              
69 6         108 my $tables = $self->tables;
70 6         84 my %info;
71 6         15 for my $table ( @{$tables} ) {
  6         20  
72 13         23 push @{$info{tables}}, $table->as_hash;
  13         64  
73             }
74              
75 6         15 for my $view ( @{ $self->views } ) {
  6         135  
76 2         16 push @{$info{views}}, $view->as_hash;
  2         11  
77             }
78              
79 6         115 my $yaml = YAML::Tiny->new;
80 6         77 $yaml->[0] = \%info;
81              
82 6         46 return $yaml->write_string;
83             }
84              
85             sub get_datatype {
86 69     69 1 131 my $self = shift;
87              
88 69         1636 my $datatypes = $self->datatypes;
89 69         718 return $datatypes->{$_[0]};
90             }
91              
92             sub _parse_tables {
93 9     9   1512 my ($self) = shift;
94              
95 9         48 $self->_parse;
96 8         3041 $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   39 my $self = shift;
108              
109 10         109 my $zip = Archive::Zip->new;
110 10 100       580 if ( $zip->read( $self->file ) != AZ_OK ) {
111 1         3130 croak "can't read file " . $self->file;
112             }
113              
114 9         17425 my $xml = $zip->contents( 'document.mwb.xml' );
115 9         19027 my $dom = XML::LibXML->load_xml( string => $xml );
116              
117 9         34106 $self->_set_dom( $dom );
118              
119 9         76 my %datatypes;
120 9         249 my @simple_type_nodes = $dom->documentElement->findnodes( './/value[@key="simpleDatatypes"]/link' );
121 9         7782 for my $type_node ( @simple_type_nodes ) {
122 390         2047 my $link = $type_node->textContent;
123 390         1532 my $datatype = uc +(split /\./, $link)[-1];
124 390         865 $datatype =~ s/_F\z//;
125              
126 390         1326 $datatypes{$link} = { name => $datatype, length => undef };
127             }
128              
129 9         156 my @user_type_structs = $dom->documentElement->findnodes( './/value[@key="userDatatypes"]' );
130 9         7039 for my $type_structs ( @user_type_structs ) {
131 9         178 my @user_types = $type_structs->findnodes( './value[@struct-name="db.UserDatatype"]' );
132 9         544 for my $type ( @user_types ) {
133 166         466 my $name = $type->findvalue( '@id' );
134 166         10012 my $sql = $type->findvalue( './value[@key="sqlDefinition"]' );
135 166         11547 my ($orig) = $sql =~ m{^([A-Z]+)};
136 166         651 my ($length) = $sql =~ m{\( (\d+) \)}x;
137 166         380 my ($precision) = $sql =~ m{\( (\d+,\d+) \)}x;
138 166         492 my ($args) = $sql =~ m{\( (.+?) \)}x;
139 166         420 my $gui_name = $type->findvalue( './value[@key="name"]' );
140              
141 166         12451 $datatypes{$name} = { name => $orig, length => $length, precision => $precision, gui_name => $gui_name, args => $args };
142             }
143             }
144              
145 9         1535 $self->_set_datatypes( \%datatypes );
146              
147 9         122 my @tables;
148              
149 9         86 my @table_nodes = $dom->documentElement->findnodes( './/value[@struct-name="db.mysql.Table"]' );
150 9         7019 for my $table_node ( @table_nodes ) {
151 27         916 push @tables, MySQL::Workbench::Parser::Table->new(
152             node => $table_node,
153             parser => $self,
154             );
155             }
156              
157 9 100       302 $self->_lint( \@tables ) if $self->lint;
158 9         245 $self->_set_tables( \@tables );
159              
160 9         105 my @views;
161              
162 9         84 my @view_nodes = $dom->documentElement->findnodes( './/value[@struct-name="db.mysql.View"]' );
163              
164 9         7830 my %column_mapping;
165 9 100       168 if ( @view_nodes ) {
166              
167             TABLE:
168 1         4 for my $table ( @tables ) {
169 2         6 my $name = $table->name;
170              
171 2         4 for my $col ( @{ $table->columns } ) {
  2         43  
172 4         27 my $col_name = $col->name;
173 4         12 $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   1169 my ($self, $tables) = @_;
191              
192 10 100       48 return if !ref $tables;
193 9 100       41 return if 'ARRAY' ne ref $tables;
194              
195 8         38 my %tablenames;
196             my %indexes;
197 8         0 my %duplicate_columns;
198              
199 8         17 for my $table ( @{ $tables } ) {
  8         29  
200 21         64 my $name = $table->name;
201              
202 21         74 $tablenames{$name}++;
203              
204             INDEX:
205 21         38 for my $index ( @{ $table->indexes } ) {
  21         391  
206 40         250 my $index_name = $index->name;
207              
208 40 100       108 next INDEX if $index_name eq 'PRIMARY';
209 19 100       60 next INDEX if $index->type eq 'UNIQUE';
210              
211 10         38 $indexes{$index_name}++;
212             }
213              
214 21         34 my %columns;
215              
216             COLUMN:
217 21         41 for my $column ( @{ $table->columns } ) {
  21         347  
218 54         242 my $column_name = $column->name;
219 54 100       147 $duplicate_columns{$name}++ if $columns{$column_name};
220 54         174 $columns{$column_name}++;
221             }
222             }
223              
224             # warn if table names occur more than once
225 8         58 my @duplicate_tables = grep{ $tablenames{$_} > 1 }sort keys %tablenames;
  20         69  
226 8 100       33 if ( @duplicate_tables ) {
227 1         239 carp 'duplicate table names (' .
228             ( join ', ', @duplicate_tables ).
229             ')';
230             }
231              
232             # warn if index name occurs more than once
233 8         146 my @duplicate_indexes = grep{ $indexes{$_} > 1 }sort keys %indexes;
  9         31  
234 8 100       34 if ( @duplicate_indexes ) {
235 1         125 carp 'duplicate indexes (' .
236             ( join ', ', @duplicate_indexes ) .
237             ')';
238             }
239              
240             # warn if there are duplicate column names
241 8 100       79 if ( %duplicate_columns ) {
242 1         98 carp 'duplicate column names in a table (' .
243             ( join ', ', sort keys %duplicate_columns ).
244             ')';
245             }
246              
247 8         74 return 1;
248             }
249              
250             1;
251              
252             __END__