File Coverage

blib/lib/Rose/DBx/Object/Builder.pm
Criterion Covered Total %
statement 226 254 88.9
branch 46 84 54.7
condition 8 20 40.0
subroutine 21 22 95.4
pod 4 4 100.0
total 305 384 79.4


line stmt bran cond sub pod time code
1             package Rose::DBx::Object::Builder;
2 1     1   41272 use strict;
  1         3  
  1         45  
3 1     1   6 use warnings;
  1         2  
  1         34  
4 1     1   6 no warnings 'recursion';
  1         7  
  1         50  
5 1     1   6 use Exporter 'import';
  1         2  
  1         43  
6              
7 1     1   12 use base qw(Rose::Object);
  1         1  
  1         1074  
8             our @EXPORT = qw(config parse build show);
9             our @EXPORT_OK = qw(config parse build show);
10              
11 1     1   1774 use Lingua::EN::Inflect 'PL';
  1         24587  
  1         144  
12 1     1   1023 use Regexp::Common;
  1         5543  
  1         6  
13 1     1   85533 use DBI;
  1         18963  
  1         6745  
14              
15             our $VERSION = 0.09;
16             # 12.9
17              
18             sub config {
19 7     7 1 880 my $self = shift;
20 7 100 66     52 unless ($self && defined $self->{CONFIG}) {
21             $self->{CONFIG} = {
22             db => {
23             name => undef,
24             type => 'mysql',
25             host => '127.0.0.1',
26             port => undef,
27             username => 'root',
28             password => 'root',
29             tables_are_singular => undef,
30             table_prefix => '',
31             options => {RaiseError => 0, PrintError => 0, AutoCommit => 1}},
32             format => {
33             expression => sub {
34 35     35   73 my $expression = lc(shift);
35 35         303 $expression =~ s/\s*,?\s*\band\b\s*,?\s*/, /g;
36 35         199 $expression =~ s/\b(a|an|the)\b//g;
37 35         57 $expression =~ s/\.//g;
38 35         82 return $expression;
39             },
40             table => sub {
41 60     60   88 my $table = shift;
42 60         292 $table =~ s/^\s+|\s+$//g;
43 60         108 $table =~ s/\s+/_/g;
44 60         189 return $table;
45             },
46             column => sub {
47 115     115   163 my $column = shift;
48 115         510 $column =~ s/^\s+|\s+$//g;
49 115         273 $column =~ s/\s+/_/g;
50 115         262 return $column;
51             },
52             },
53 1         65 table => {
54             mysql => 'CREATE TABLE [% table_name %] ([% columns %]) TYPE=INNODB;',
55             Pg => 'CREATE TABLE [% table_name %] ([% columns %]);',
56             SQLite => 'CREATE TABLE [% table_name %] ([% columns %]);',
57             },
58             primary_key => {
59             name => 'id',
60             type => {
61             mysql => 'INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY',
62             Pg => 'SERIAL PRIMARY KEY',
63             SQLite => 'INTEGER NOT NULL PRIMARY KEY',
64             }
65             },
66             foreign_key => {
67             suffix => '_id',
68             type => {
69             mysql => 'INTEGER',
70             Pg => 'INTEGER',
71             SQLite => 'INTEGER',
72             },
73             singular => 1,
74             clause => 'FOREIGN KEY ([% foreign_key %]) REFERENCES [% reference_table %] ([% reference_primary_key %]) ON UPDATE CASCADE ON DELETE CASCADE',
75             },
76             add_clause => 'ALTER TABLE [% table_name %] ADD [% clause %];',
77             map_table => '[% table_name %]_[% foreign_table_name %]_map',
78             columns => {
79             name => 'VARCHAR(255)',
80             unique => 'VARCHAR(255) UNIQUE',
81             required => 'VARCHAR(255) NOT NULL',
82             text => 'TEXT',
83             integer => 'INTEGER',
84             number => 'NUMERIC',
85             date => 'DATE',
86             time => 'TIME',
87             timestamp => 'TIMESTAMP',
88             money => 'DECIMAL(13,2)',
89             boolean => 'BOOLEAN',
90             }
91             };
92            
93 1         5 $self->{CONFIG}->{columns}->{title} = $self->{CONFIG}->{columns}->{name};
94 1         5 $self->{CONFIG}->{columns}->{description} = $self->{CONFIG}->{columns}->{text};
95 1         3 $self->{CONFIG}->{columns}->{percentage} = $self->{CONFIG}->{columns}->{number};
96 1         4 $self->{CONFIG}->{columns}->{cost} = $self->{CONFIG}->{columns}->{money};
97 1         5 $self->{CONFIG}->{columns}->{price} = $self->{CONFIG}->{columns}->{money};
98 1         4 $self->{CONFIG}->{columns}->{username} = $self->{CONFIG}->{columns}->{unique};
99             }
100            
101 7 100       22 if (@_) {
102 1         2 my $config = shift;
103 1         2 foreach my $hash (keys %{$config}) {
  1         4  
104 1 50       5 if (ref $config->{$hash} eq 'HASH') {
105 1         1 foreach my $key (keys %{$config->{$hash}}) {
  1         4  
106 1 50       5 if (ref $config->{$hash}->{$key} eq 'HASH') {
107 0         0 foreach my $sub_key (keys %{$config->{$hash}->{$key}}) {
  0         0  
108 0         0 $self->{CONFIG}->{$hash}->{$key}->{$sub_key} = $config->{$hash}->{$key}->{$sub_key};
109             }
110             }
111             else {
112 1         7 $self->{CONFIG}->{$hash}->{$key} = $config->{$hash}->{$key};
113             }
114             }
115             }
116             else {
117 0         0 $self->{CONFIG}->{$hash} = $config->{$hash};
118             }
119             }
120             }
121            
122 7         21 return $self->{CONFIG};
123             }
124              
125             sub build {
126 0     0 1 0 my $self = shift;
127 0         0 my $dbh = shift;
128 0         0 my $config = $self->config;
129 0         0 my $schema = $self->parse;
130 0 0       0 return unless $schema;
131              
132 0 0       0 unless ($dbh) {
133 0 0       0 die "Database name missing" unless $config->{db}->{name};
134 0         0 my $host;
135 0 0       0 $host = 'host='. $config->{db}->{host} if $config->{db}->{host};
136 0 0       0 $host .= ';port='.$config->{db}->{port} if $config->{db}->{port};
137 0         0 my $dsn = qq(dbi:$config->{db}->{type}:dbname=$config->{db}->{name};$host);
138 0 0       0 $dbh = DBI->connect($dsn, $config->{db}->{username}, $config->{db}->{password}, $config->{db}->{options}) or die "Error opening database: $config->{db}->{name}\n";
139             }
140            
141 0         0 eval {
142 0         0 foreach my $sql (split /;/, $schema) {
143 0 0       0 $dbh->do($sql) or warn "Error executing SQL: $sql;\n";
144             }
145 0 0       0 $dbh->commit unless $config->{db}->{options}->{AutoCommit};
146             };
147              
148 0 0       0 if ($@) {
149 0         0 warn "Transaction aborted: $@";
150 0         0 eval {$dbh->rollback};
  0         0  
151             }
152            
153 0 0       0 $dbh->disconnect or die "Error closing database: $config->{db}->{name}\n";
154             }
155              
156             sub parse {
157 5     5 1 2062 my $self = shift;
158 5         9 my $string = shift;
159              
160 5 50       17 if ($string) {
161 5         16 my $config = $self->config;
162 5         31 foreach my $expression (split /\./, $string) {
163 30         43 my $schema;
164 30 100       243 if ($expression =~ /\s+as\s+/) {
    100          
    100          
165 5         15 $schema = _as ($config, $expression);
166             }
167             elsif ($expression =~ /vice[\s\-]+versa/) {
168 5         15 $schema = _many_to_many ($config, $expression);
169             }
170             elsif ($expression =~ /(has|have)\s+many/) {
171 5         20 $schema = _has_many ($config, $expression);
172             }
173             else {
174 15         41 $schema = _has_a ($config, $expression);
175             }
176 30 50       155 $self->{SCHEMA} .= $schema if $schema;
177             }
178             }
179 5   50     56 return $self->{SCHEMA} || '';
180             }
181              
182             sub show {
183 1     1 1 736 my $self = shift;
184 1         6 my $schema = $self->parse(@_);
185 1 50       6 return unless $schema;
186 1         2 my @pretty;
187            
188 1         9 foreach my $schema (split /;/, $schema) {
189 9 100       30 if ($schema =~ /CREATE/) {
190 5         37 $schema =~ s/^([^\(]+)\(/$1\(\n\t/g;
191 5         67 $schema =~ s/\)([^\)]+)$/\n\)$1/g;
192 5         71 $schema =~ s/([^\d]),([^\d])/$1,\n\t$2/g;
193 5         11 $schema =~ s/\)$/\n)/;
194             }
195 9         28 push @pretty, $schema . ';';
196             }
197 1         13 return join "\n\n", @pretty;
198             }
199              
200             sub _as {
201 5     5   10 my $config = shift;
202 5         17 my $expression = $config->{format}->{expression}->(shift);
203 5         55 my ($table_name, $has, $foreign_table_name, $foreign_key) = split /\s+(has|have)\s+(.*)\s+as\s+(.*)/, $expression;
204 5 50 33     44 return unless $table_name && $foreign_table_name && $foreign_key;
      33        
205 5         21 $table_name = _normalise_table($config, $config->{format}->{table}->($table_name));
206 5         19 $foreign_table_name = _normalise_table($config, $config->{format}->{table}->($foreign_table_name));
207 5         13 $foreign_key = $config->{format}->{column}->($foreign_key);
208 5 50       19 $foreign_key = $config->{foreign_key}->{singular} ? _singularise($foreign_key) : $foreign_key;
209 5         14 $foreign_key .= $config->{foreign_key}->{suffix};
210            
211 5         12 my $add_column = $config->{add_clause};
212 5         26 $add_column =~ s/\[%\s*table_name\s*%\]/$table_name/;
213 5         11 $add_column =~ s/\[%\s*table_name\s*%\]/$table_name/;
214            
215 5         20 my $foreign_key_column = $foreign_key . ' ' . $config->{foreign_key}->{type}->{$config->{db}->{type}};
216 5         28 $add_column =~ s/\[%\s*clause\s*%\]/$foreign_key_column/;
217 5         9 my $schema = $add_column;
218              
219 5         73 my $add_foreign_key = $config->{add_clause};
220 5         24 $add_foreign_key =~ s/\[%\s*table_name\s*%\]/$table_name/;
221 5         14 my $foreign_key_clause = _generate_foreign_key_clause($config, $foreign_key, $foreign_table_name);
222 5         26 $add_foreign_key =~ s/\[%\s*clause\s*%\]/$foreign_key_clause/;
223 5         48 $schema .= $add_foreign_key;
224            
225 5         18 return $schema;
226             }
227              
228             sub _many_to_many {
229 5     5   13 my $config = shift;
230 5         14 my $expression = $config->{format}->{expression}->(shift);
231 5         79 $expression =~ s/,\s+vice[\s\-]+versa//;
232 5         48 my ($table_name, $has, $foreign_table_name) = split /\s*(has|have)\s+many\s*/, $expression;
233 5 50 33     28 return unless $table_name && $foreign_table_name;
234 5         17 $table_name = _normalise_table($config, $config->{format}->{table}->($table_name));
235 5         18 $foreign_table_name = _normalise_table($config, $config->{format}->{table}->($foreign_table_name));
236 5         44 my $map_table = $config->{map_table};
237 5         33 $map_table =~ s/\[%\s*table_name\s*%\]/$table_name/;
238 5         26 $map_table =~ s/\[%\s*foreign_table_name\s*%\]/$foreign_table_name/;
239            
240 5         20 my $schema = $config->{table}->{$config->{db}->{type}};
241 5         26 $schema =~ s/\[%\s*table_name\s*%\]/$map_table/;
242            
243 5         9 my $foreign_keys;
244 5         26 my @columns = ($config->{primary_key}->{name} . ' ' . $config->{primary_key}->{type}->{$config->{db}->{type}});
245            
246 5         14 foreach my $table ($table_name, $foreign_table_name) {
247 10 50       36 my $foreign_key = $config->{foreign_key}->{singular} ? _singularise($table) : $table;
248 10         23 $foreign_key .= $config->{foreign_key}->{suffix};
249 10         28 $foreign_keys->{$foreign_key} = $table;
250 10         52 push @columns, $foreign_key . ' ' . $config->{foreign_key}->{type}->{$config->{db}->{type}};
251             }
252            
253 5         10 foreach my $foreign_key (keys %{$foreign_keys}) {
  5         19  
254 10         23 push @columns, _generate_foreign_key_clause($config, $foreign_key, $foreign_keys->{$foreign_key});
255             }
256            
257 5         24 my $schema_columns = join ',', @columns;
258 5         29 $schema =~ s/\[%\s*columns\s*%\]/$schema_columns/;
259 5         28 return $schema;
260             }
261              
262             sub _has_many {
263 5     5   9 my $config = shift;
264 5         18 my $expression = $config->{format}->{expression}->(shift);
265 5         49 my ($table_name, $has, $foreign_table_name) = split /\s*(has|have)\s+many\s*/, $expression;
266 5 50 33     31 return unless $table_name && $foreign_table_name;
267 5         19 $table_name = _normalise_table($config, $config->{format}->{table}->($table_name));
268 5         15 $foreign_table_name = _normalise_table($config, $config->{format}->{table}->($foreign_table_name));
269 5         13 my $add_column = $config->{add_clause};
270 5         25 $add_column =~ s/\[%\s*table_name\s*%\]/$foreign_table_name/;
271            
272 5 50       21 my $foreign_key = $config->{foreign_key}->{singular} ? _singularise($table_name) : $table_name;
273 5         52 $foreign_key .= $config->{foreign_key}->{suffix};
274            
275 5         20 my $foreign_key_column = $foreign_key . ' ' . $config->{foreign_key}->{type}->{$config->{db}->{type}};
276 5         26 $add_column =~ s/\[%\s*clause\s*%\]/$foreign_key_column/;
277 5         9 my $schema = $add_column;
278            
279 5         9 my $add_foreign_key = $config->{add_clause};
280 5         24 $add_foreign_key =~ s/\[%\s*table_name\s*%\]/$foreign_table_name/;
281            
282 5         12 my $foreign_key_clause = _generate_foreign_key_clause($config, $foreign_key, $table_name);
283 5         26 $add_foreign_key =~ s/\[%\s*clause\s*%\]/$foreign_key_clause/;
284 5         13 $schema .= $add_foreign_key;
285            
286 5         26 return $schema;
287             }
288              
289             sub _has_a {
290 20     20   29 my $config = shift;
291 20         52 my $expression = $config->{format}->{expression}->(shift);
292 20         157 my ($table_name, $has, $columns) = ($expression =~ /^([\w_\-0-9\s]+)\s+(has|have)\s+(.*)$/);
293 20 50 33     110 return unless $table_name && $columns;
294            
295 20         23 my ($schema, $foreign_keys, $foreign_table_name, $foreign_table_columns, $custom_columns);
296 20         38 my $foreign_key_suffix = $config->{foreign_key}->{suffix};
297 20         49 my $table = {name => _normalise_table($config, $config->{format}->{table}->($table_name))};
298            
299 20         31 push @{$table->{columns}}, {name => $config->{primary_key}->{name}, type => $config->{primary_key}->{type}->{$config->{db}->{type}}};
  20         120  
300            
301 20         79 while ($columns =~ /[()]/) {
302 35         4776 ($foreign_table_name, $foreign_table_columns) = ($columns =~ /([\w_\-0-9\s]+)\s*($RE{balanced}{-parens=>'()'})/);
303 35         5948 ($foreign_table_columns) = ($foreign_table_columns =~ /\((.*)\)/);
304            
305 35 100       125 if($foreign_table_columns =~ /^\s*(has|have)/) {
306 5         29 $schema .= _has_a($config, join ' ', ($foreign_table_name , $foreign_table_columns));
307 5         22 $foreign_table_name = _normalise_table($config, $config->{format}->{table}->($foreign_table_name));
308 5 50       20 my $foreign_key = $config->{foreign_key}->{singular} ? _singularise($foreign_table_name) : $foreign_table_name;
309 5         11 $foreign_key .= $foreign_key_suffix;
310 5         13 $foreign_keys->{$foreign_key} = $foreign_table_name;
311 5         28 $columns =~ s/(\b[\w_\-0-9\s]*)\b\s*($RE{balanced}{-parens=>'()'})/$foreign_key/;
312             }
313             else {
314 30         83 $foreign_table_name = $config->{format}->{column}->($foreign_table_name);
315            
316 30 100       128 if ($foreign_table_columns =~ /^reference/) {
    50          
317 5         10 my $foreign_key = $foreign_table_name;
318 5         27 my ($reference_table) = ($foreign_table_columns =~ /^references?\s+([\w_\-0-9\s]+)$/);
319            
320 5 50       16 if ($reference_table) {
321 5         16 $foreign_table_name = _normalise_table($config, $config->{format}->{table}->($reference_table));
322             }
323             else {
324 0         0 $foreign_table_name =~ s/$foreign_key_suffix$//;
325 0         0 $foreign_table_name = _normalise_table($config, $config->{format}->{table}->($foreign_table_name));
326             }
327            
328 5         19 $foreign_keys->{$foreign_key} = $foreign_table_name;
329             }
330             elsif (exists $config->{columns}->{$foreign_table_columns}) {
331 25         84 $custom_columns->{$foreign_table_name} = $config->{columns}->{$foreign_table_columns};
332             }
333            
334 30         139 $columns =~ s/([\w_\-0-9]*)\s*($RE{balanced}{-parens=>'()'})/$1/; # clean it for the while loop
335             }
336             }
337            
338 20         2619 foreach my $column (split /\s*,\s*/, $columns) {
339 80         184 $column = $config->{format}->{column}->($column);
340 80 100       177 if (exists $foreign_keys->{$column}) {
341 10         14 push @{$table->{columns}}, {name => $column, type => $config->{foreign_key}->{type}->{$config->{db}->{type}}};
  10         67  
342             }
343             else {
344 70 100       191 if (exists $custom_columns->{$column}) {
    100          
345 25         28 push @{$table->{columns}}, {name => $column, type => $custom_columns->{$column}};
  25         111  
346             }
347             elsif (exists $config->{columns}->{$column}) {
348 20         26 push @{$table->{columns}}, {name => $column, type => $config->{columns}->{$column}};
  20         111  
349             }
350             else {
351 25         28 my $column_type;
352 25         29 DEF: foreach my $column_key (keys %{$config->{columns}}) {
  25         119  
353 310 100       2632 if ($column =~ /$column_key/) {
354             # first match
355 20         28 $column_type = $column_key;
356 20         43 last DEF;
357             }
358             }
359            
360 25 100       81 if ($column_type) {
361 20         29 push @{$table->{columns}}, {name => $column, type => $config->{columns}->{$column_type}};
  20         115  
362             }
363             else {
364 5         9 push @{$table->{columns}}, {name => $column, type => $config->{columns}->{name}}; # default
  5         28  
365             }
366             }
367             }
368             }
369            
370 20         39 my $schema_columns = [map {$_->{name} . ' ' . $_->{type}} @{$table->{columns}}];
  100         275  
  20         43  
371            
372 20         31 foreach my $foreign_key (keys %{$foreign_keys}) {
  20         61  
373 10         13 push @{$schema_columns}, _generate_foreign_key_clause($config, $foreign_key, $foreign_keys->{$foreign_key});
  10         34  
374             }
375            
376 20         33 my $schema_columns_string = join ',', @{$schema_columns};
  20         63  
377            
378 20         60 $schema .= $config->{table}->{$config->{db}->{type}};
379 20         180 $schema =~ s/\[%\s*table_name\s*%\]/$table->{name}/;
380 20         102 $schema =~ s/\[%\s*columns\s*%\]/$schema_columns_string/;
381            
382 20         193 return $schema;
383             }
384              
385             sub _singularise {
386             # based on Rose::DB::Object::ConventionManager
387 85     85   122 my $word = shift;
388 85         170 $word =~ s/ies$/y/i;
389 85 50       215 return $word if ($word =~ s/ses$/s/);
390 85 50       256 return $word if($word =~ /[aeiouy]ss$/i);
391 85         178 $word =~ s/s$//i;
392 85         207 return $word;
393             }
394              
395             sub _generate_foreign_key_clause {
396 30     30   59 my ($config, $foreign_key, $reference_table) = @_;
397 30         56 my $foreign_key_clause = $config->{foreign_key}->{clause};
398 30         144 $foreign_key_clause =~ s/\[%\s*foreign_key\s*%\]/$foreign_key/;
399 30         144 $foreign_key_clause =~ s/\[%\s*reference_table\s*%\]/$reference_table/;
400 30         183 $foreign_key_clause =~ s/\[%\s*reference_primary_key\s*%\]/$config->{primary_key}->{name}/;
401 30         121 return $foreign_key_clause;
402             }
403              
404             sub _normalise_table {
405 60     60   107 my ($config, $table) = @_;
406 60         149 my $table_name;
407            
408 60 50       154 if ($config->{db}->{tables_are_singular}) {
409 60         104 $table_name = _singularise($table);
410             }
411             else {
412 0         0 $table_name = Lingua::EN::Inflect::PL(_singularise($table));
413             }
414            
415 60 100       234 return $config->{db}->{table_prefix} . $table_name if defined $config->{db}->{table_prefix};
416 36         98 return $table_name;
417             }
418              
419             1;
420              
421             __END__