File Coverage

blib/lib/DBIx/Class/ResultDDL/SchemaLoaderMixin.pm
Criterion Covered Total %
statement 133 150 88.6
branch 48 78 61.5
condition 26 50 52.0
subroutine 25 27 92.5
pod 4 4 100.0
total 236 309 76.3


line stmt bran cond sub pod time code
1             package DBIx::Class::ResultDDL::SchemaLoaderMixin;
2 1     1   590752 use strict;
  1         10  
  1         32  
3 1     1   6 use warnings;
  1         2  
  1         31  
4 1     1   11 use List::Util 'max', 'all';
  1         3  
  1         65  
5 1     1   576 use DBIx::Class::ResultDDL;
  1         3  
  1         7  
6 1     1   104 use Carp;
  1         2  
  1         64  
7             sub deparse; #local utilities to be cleaned from the namespace
8             sub deparse_hashkey;
9 1     1   6 use namespace::clean;
  1         10  
  1         7  
10              
11             # ABSTRACT: Modify Schema Loader to generate ResultDDL notation
12             our $VERSION = '2.03'; # VERSION
13              
14              
15             #sub _write_classfile {
16             # my ($self, $class, $text, $is_schema)= @_;
17             # main::explain($class);
18             # main::explain($text);
19             # main::explain($self->{_dump_storage}{$class});
20             # $self->next::method($class, $text, $is_schema);
21             #}
22              
23             sub generate_resultddl_import_line {
24 6     6 1 29 qq|use DBIx::Class::ResultDDL qw/ -V2 /;\n|
25             }
26              
27              
28             sub generate_column_info_sugar {
29 20     20 1 249 my ($self, $class, $col_name, $orig_col_info)= @_;
30              
31 20         66 my $checkpkg= $self->_get_class_check_namespace($class);
32 20         74 my $class_settings= DBIx::Class::ResultDDL::_settings_for_package($checkpkg);
33              
34 20         102 my %col_info= %$orig_col_info;
35 20         67 my $stmt= _get_data_type_sugar(\%col_info, $class_settings);
36             $stmt .= ' null'
37 20 100       69 if delete $col_info{is_nullable};
38             $stmt .= ' default('.deparse(delete $col_info{default_value}).'),'
39 20 100       62 if exists $col_info{default_value};
40             # add sugar for inflate_json if the serializer class is JSON, but not if the package feature inflate_json
41             # was enabled and the column type is flagged as json.
42 20 50 50     100 $stmt .= ' inflate_json' if 'JSON' eq ($col_info{serializer_class}||'');
43 20 100       107 $stmt .= ' fk' if delete $col_info{is_foreign_key};
44            
45             # Test the syntax for equality to the original
46 20         38 my $out;
47 20         1745 eval "package $checkpkg; \$out= DBIx::Class::ResultDDL::expand_col_options(\$checkpkg, $stmt);";
48 20 50       110 defined $out or croak "Error verifying generated ResultDDL for $class $col_name: $@";
49            
50 20 50       67 if ($out->{'extra.unsigned'}) {
51 0         0 $out->{extra}{unsigned}= delete $out->{'extra.unsigned'};
52             }
53              
54             # Ignore the problem where 'integer' generates a default size for mysql that wasn't
55             # in the Schema Loader spec. TODO: add an option to skip generating this.
56 20 100 100     87 delete $out->{size} if $out->{size} && !$orig_col_info->{size};
57              
58             # Data::Dumper gets confused and thinks sizes need quoted
59 20 100 66     111 if (defined $orig_col_info->{size} && $orig_col_info->{size} =~ /^[0-9]+$/) {
60 8         33 $orig_col_info->{size}= 0 + $orig_col_info->{size};
61             }
62              
63 20 50       115 if (deparse({ %col_info, %$out }) eq deparse({ %$orig_col_info })) {
64             # Any field in %$out removes the need to have it in $col_info.
65             # This happens with implied options like serializer_class => 'JSON'
66 20         64 for (keys %col_info) {
67 4 50       46 delete $col_info{$_} if exists $out->{$_};
68             }
69             # remove trailing comma
70 20         81 $stmt =~ s/,\s*$//;
71             # dump the rest, and done.
72             $stmt .= ', '.&_deparse_hashkey.' => '.deparse($col_info{$_})
73 20         66 for sort keys %col_info;
74             }
75             else {
76 0         0 warn "Unable to use ResultDDL sugar '$stmt'\n "
77             .deparse({ %col_info, %$out })." ne ".deparse($orig_col_info)."\n";
78             $stmt= join(', ',
79 0         0 map &_deparse_hashkey.' => '.deparse($orig_col_info->{$_}),
80             sort keys %$orig_col_info
81             );
82             }
83 20         176 return $stmt;
84             }
85              
86              
87             sub generate_relationship_sugar {
88 4     4 1 21 my ($self, $class, $method, $relname, $foreignclass, $colmap, $options)= @_;
89             #use DDP; &p(['before', @_[1..$#_]]);
90 4         10 my $expr= '';
91             # The $foreignclass $colmap arguments can be combined into a simpler
92             # hashref of { local_col => 'ForeignClass.colname' } as long as some expectations hold:
93 4         37 my ($parent_ns)= ($class =~ /^(.*?::)([^:]+)$/);
94 4 50 33     54 if (defined $parent_ns and !ref $foreignclass and (!ref $colmap || ref $colmap eq 'HASH')) {
      33        
      33        
95             # Can we use a shortened class name for the foreign table?
96 4 50 33     49 if ($foreignclass =~ /^(.*?::)([^:]+)$/ and $1 eq $parent_ns) {
97 4         12 $foreignclass= $2;
98             }
99 4 50       26 my %newmap= ref $colmap eq 'HASH'? (%$colmap) : ($colmap => $colmap);
100             # Just in case SchemaLoader prefixed them with 'self.' or 'foreign.'...
101 4         28 s/^self[.]// for values %newmap;
102 4         16 %newmap= reverse %newmap;
103 4         22 s/^foreign[.]// for values %newmap;
104             # Apply the foreign class name to the first column in the map
105 4         14 my ($first_key)= sort keys %newmap;
106 4         17 $newmap{$first_key}= $foreignclass . '.' . $newmap{$first_key};
107 4         21 $expr .= deparse(\%newmap);
108             } else {
109 0         0 $expr .= deparse($foreignclass, $colmap);
110             }
111 4 50 33     45 if ($options && keys %$options) {
112 4         31 $expr .= ', ' . $self->generate_relationship_attr_sugar($options);
113             }
114              
115             # Test the syntax for equality to the original
116 4         27 my $checkpkg= $self->_get_class_check_namespace($class);
117 4         12 my @out;
118 4         505 eval "package $checkpkg; \@out= DBIx::Class::ResultDDL::expand_relationship_params(\$class, \$method, \$relname, $expr);";
119 4 50       32 @out or croak "Error verifying generated ResultDDL for $class $method $relname: $@";
120              
121             #use DDP; &p(['after', @out, $expr]);
122              
123 4         27 return $method . ' ' . deparse_hashkey($relname) . ' => ' . $expr . ';';
124             }
125              
126              
127             sub generate_relationship_attr_sugar {
128 4     4 1 12 my ($self, $orig_options)= @_;
129 4         18 my %options= %$orig_options;
130 4         11 my @expr;
131 4 50 66     35 if (defined $options{on_update} && defined $options{on_delete}
      66        
132             && $options{on_update} eq $options{on_delete}
133             ) {
134 2         10 my $val= delete $options{on_update};
135 2         6 delete $options{on_delete};
136 2 0       14 push @expr, $val eq 'CASCADE'? 'ddl_cascade'
    50          
137             : $val eq 'RESTRICT'? 'ddl_cascade(0)'
138             : 'ddl_cascade('.deparse($val).')'
139             }
140 4 50 66     53 if (defined $options{cascade_copy} && defined $options{cascade_delete}
      66        
141             && $options{cascade_copy} eq $options{cascade_delete}
142             ) {
143 2         9 my $val= delete $options{cascade_copy};
144 2         7 delete $options{cascade_delete};
145 2 50       15 push @expr, $val eq '1'? 'dbic_cascade'
146             : 'dbic_cascade('.deparse($val).')'
147             }
148 4 100       19 push @expr, substr(deparse(\%options),2,-2) if keys %options;
149 4         31 return join ', ', @expr
150             }
151              
152             my %rel_methods= map +($_ => 1), qw( belongs_to might_have has_one has_many );
153             sub _dbic_stmt {
154 22     22   657485 my ($self, $class, $method)= splice(@_, 0, 3);
155 22 100       185 $self->{_MyLoader_use_resultddl}{$class}++
156             or $self->_raw_stmt($class, $self->generate_resultddl_import_line);
157 22 100 33     197 if ($method eq 'table') {
    100          
    100          
    50          
158 6         26 $self->_raw_stmt($class, q|table |.deparse(@_).';');
159             }
160             elsif ($method eq 'add_columns') {
161 6         16 my @col_defs;
162 6         23 while (@_) {
163 20         67 my ($col_name, $col_info)= splice(@_, 0, 2);
164 20         56 push @col_defs, [
165             deparse_hashkey($col_name),
166             $self->generate_column_info_sugar($class, $col_name, $col_info)
167             ];
168             }
169             # align the definitions, but round up to help avoid unnecessary diffs
170             # when new columns get added.
171 6         46 my $widest= max map length($_->[0]), @col_defs;
172 6         20 $widest= ($widest + 3) & ~3;
173             $self->_raw_stmt($class, sprintf("col %-*s => %s;", $widest, @$_))
174 6         80 for @col_defs;
175             }
176             elsif ($method eq 'set_primary_key') {
177 6         19 $self->_raw_stmt($class, q|primary_key |.deparse(@_).';');
178             }
179             elsif ($rel_methods{$method} && @_ == 4) {
180 4         33 $self->_raw_stmt($class, $self->generate_relationship_sugar($class, $method, @_));
181             }
182             else {
183 0         0 $self->next::method($class, $method, @_);
184             }
185 22         335 return;
186             }
187              
188             my %data_type_sugar= (
189             (map {
190             my $type= $_;
191             $type => sub { my ($col_info)= @_;
192             if ($col_info->{size} && $col_info->{size} =~ /^[0-9]+$/) {
193             return "$type(".delete($col_info->{size})."),";
194             } elsif ($col_info->{size} && ref $col_info->{size} eq 'ARRAY'
195             && ($#{$col_info->{size}} == 0 || $#{$col_info->{size}} == 1)
196             && (all { /^[0-9]+$/ } @{$col_info->{size}})
197             ) {
198             return "$type(".join(',', @{delete($col_info->{size})})."),";
199             } else {
200             return $type;
201             }
202             }
203             } qw( integer float real numeric decimal varchar char )),
204             (map {
205             my $type= $_;
206             $type => sub { my ($col_info, $class_settings)= @_;
207             # include timezone in type sugar, if known.
208             if ($col_info->{timezone} && !ref $col_info->{timezone}) {
209             return "$type(".deparse(delete $col_info->{timezone})."),";
210             } else {
211             return $type;
212             }
213             }
214             } qw( datetime timestamp )),
215             (map {
216             my $type= $_;
217             $type => sub { my ($col_info, $class_settings)= @_;
218             # Remove serializer_class => 'JSON' if inflate_json is enabled package-wide
219             delete $col_info->{serializer_class}
220             if $class_settings->{inflate_json} && ($col_info->{serializer_class}||'') eq 'JSON';
221             return $type;
222             }
223             } qw( json jsonb )),
224             );
225              
226             sub _get_data_type_sugar {
227 20     20   50 my ($col_info, $class_settings)= @_;
228              
229             my $t= delete $col_info->{data_type}
230 20 50       71 or return ();
231              
232 20   66     123 my $pl= ($data_type_sugar{$t} //= do {
233 1         9 my $sugar= DBIx::Class::ResultDDL->can($t);
234 1 50       7 my @out= $sugar? $sugar->() : ();
235 2     2   11 @out >= 2 && $out[0] eq 'data_type' && $out[1] eq $t? sub { $t }
236 0     0   0 : sub { 'data_type => '.deparse($t).',' }
237 1 50 33     36 })->($col_info, $class_settings);
238              
239 20 0 33     95 if ($col_info->{extra} && $col_info->{extra}{unsigned}) {
240 0 0       0 $pl =~ s/,?$/,/ unless $pl =~ /\w$/;
241 0         0 $pl .= ' unsigned';
242 0 0       0 if (1 == keys %{ $col_info->{extra} }) {
  0         0  
243 0         0 delete $col_info->{extra};
244             } else {
245 0         0 $col_info->{extra}= { %{ $col_info->{extra} } };
  0         0  
246 0         0 delete $col_info->{extra}{unsigned};
247             }
248             }
249 20         49 return $pl;
250             }
251              
252             sub _deparse_scalar {
253 143 100   143   827 return $_ if /^(0|[1-9][0-9]*)$/;
254 67         131 my $x= $_;
255 67         145 $x =~ s/\\/\\\\/g;
256 67         109 $x =~ s/'/\\'/g;
257 67         378 return "'$x'";
258             }
259             sub _deparse_scalarref {
260 6     6   16 "\\" . (map &_deparse_scalar, $$_)[0]
261             }
262             sub deparse_hashkey { local $_= $_[0]; &_deparse_hashkey }
263             sub _deparse_hashkey {
264             # TODO: complete support for perl's left-hand of => operator parsing rule
265 148 50   148   792 /^[A-Za-z_][A-Za-z0-9_]*$/? $_ : &_deparse_scalar;
266             }
267             sub _deparse_hashref {
268 46     46   80 my $h= $_;
269 46         189 return '{ '.join(', ', map +(&_deparse_hashkey.' => '.deparse($h->{$_})), sort keys %$h).' }'
270             }
271             sub _deparse_arrayref {
272 0     0   0 return '[ '.join(', ', map &_deparse, @$_).' ]'
273             }
274             sub _deparse {
275             !ref()? &_deparse_scalar
276             : ref() eq 'SCALAR'? &_deparse_scalarref
277             : ref() eq 'ARRAY'? &_deparse_arrayref
278             : ref() eq 'HASH'? &_deparse_hashref
279 189 50   189   569 : do {
    50          
    100          
    100          
280 0         0 require Data::Dumper;
281 0         0 Data::Dumper->new([$_])->Terse(1)->Quotekeys(0)->Sortkeys(1)->Indent(0)->Dump;
282             }
283             }
284             sub deparse {
285             join(', ', map &_deparse, @_);
286             }
287              
288             our %per_class_check_namespace;
289             sub _get_class_check_namespace {
290 24     24   58 my ($self, $class)= @_;
291 24   66     102 return ($per_class_check_namespace{$class} ||= do {
292 6         24 my $use_line= $self->generate_resultddl_import_line;
293 6         25 local $DBIx::Class::ResultDDL::DISABLE_AUTOCLEAN= 1;
294 6         25 my $pkg= 'DBIx::Class::ResultDDL_check' . scalar keys %per_class_check_namespace;
295 6         23 my $perl= "package $pkg; $use_line 1";
296 1 50   1   10 eval $perl or croak "Error setting up package to verify generated ResultDDL: $@\nFor code:\n$perl";
  1     1   4  
  1     1   16  
  1     1   10  
  1     1   2  
  1     1   6  
  1         8  
  1         3  
  1         7  
  1         20  
  1         3  
  1         14  
  1         16  
  1         8  
  1         8  
  1         10  
  1         6  
  1         6  
  6         716  
297 6         47 $pkg;
298             });
299             }
300              
301              
302             1;
303              
304             __END__