File Coverage

blib/lib/DBIx/Class/Schema/SanityChecker.pm
Criterion Covered Total %
statement 101 113 89.3
branch 27 44 61.3
condition 7 17 41.1
subroutine 16 18 88.8
pod 4 10 40.0
total 155 202 76.7


line stmt bran cond sub pod time code
1             package DBIx::Class::Schema::SanityChecker;
2              
3 5     5   5303 use strict;
  5         12  
  5         147  
4 5     5   29 use warnings;
  5         12  
  5         161  
5              
6 5         385 use DBIx::Class::_Util qw(
7             dbic_internal_try refdesc uniq serialize
8             describe_class_methods emit_loud_diag
9 5     5   25 );
  5         9  
10 5     5   35 use DBIx::Class ();
  5         11  
  5         87  
11 5     5   21 use Scalar::Util qw( blessed refaddr );
  5         10  
  5         219  
12 5     5   26 use namespace::clean;
  5         11  
  5         42  
13              
14             =head1 NAME
15              
16             DBIx::Class::Schema::SanityChecker - Extensible "critic" for your Schema class hierarchy
17              
18             =head1 SYNOPSIS
19              
20             package MyApp::Schema;
21             use base 'DBIx::Class::Schema';
22              
23             # this is the default setting
24             __PACKAGE__->schema_sanity_checker('DBIx::Class::Schema::SanityChecker');
25             ...
26              
27             =head1 DESCRIPTION
28              
29             This is the default implementation of the Schema and related classes
30             L.
31              
32             The validator is B. See L
33             for discussion of the runtime effects.
34              
35             Use of this class begins by invoking L
36             (usually via L), which in turn starts
37             invoking validators I> in the order listed in
38             L. For each set of returned errors (if any)
39             I> is called and the resulting strings are
40             passed to L, where final headers are prepended and the entire
41             thing is printed on C.
42              
43             The class does not provide a constructor, due to the lack of state to be
44             passed around: object orientation was chosen purely for the ease of
45             overriding parts of the chain of events as described above. The general
46             pattern of communicating errors between the individual methods (both
47             before and after formatting) is an arrayref of hash references.
48              
49             =head2 WHY
50              
51             DBIC existed for more than a decade without any such setup validation
52             fanciness, let alone something that is enabled by default (which in turn
53             L). The reason for this relatively
54             drastic change is a set of revamps within the metadata handling framework,
55             in order to resolve once and for all problems like
56             L,
57             L, etc. While
58             DBIC internals are now way more robust than they were before, this comes at
59             a price: some non-issues in code that has been working for a while, will
60             now become hard to explain, or if you are unlucky: B.
61              
62             Thus, in order to protect existing codebases to the fullest extent possible,
63             the executive decision (and substantial effort) was made to introduce this
64             on-by-default setup validation framework. A massive amount of work has been
65             invested ensuring that none of the builtin checks emit a false-positive:
66             each and every complaint made by these checks B.
67              
68             =head2 Performance considerations
69              
70             First of all - after your connection has been established - there is B
71             runtime penalty> whenever the checks are enabled.
72              
73             By default the checks are triggered every time
74             L is called. Thus there is a
75             noticeable startup slowdown, most notably during testing (each test is
76             effectively a standalone program connecting anew). As an example the test
77             execution phase of the L C distribution
78             suffers a consistent slowdown of about C<16%>. This is considered a relatively
79             small price to pay for the benefits provided.
80              
81             Nevertheless, there are valid cases for disabling the checks during
82             day-to-day development, and having them run only during CI builds. In fact
83             the test suite of DBIC does exactly this as can be seen in
84             F:
85              
86             ~/dbic_repo$ git show 39636786 | perl -ne "print if 16..61"
87              
88             Whatever you do, B: it is not
89             worth the risk.
90              
91             =head3 Perl5.8
92              
93             The situation with perl interpreters before C is sadly more
94             complicated: due to lack of built-in L, the
95             mechanism used to interrogate various classes is
96             L<< B slower|https://github.com/dbsrgits/dbix-class/commit/296248c3 >>.
97             As a result the very same version of L
98             L takes a C> hit on its
99             test execution time (these numbers are observed with the speedups of
100             L available, without them the slowdown reaches the whopping
101             C<350%>).
102              
103             It is the author's B recommendation to find a way to run the
104             checks on your codebase continuously, even if it takes much longer. Refer to
105             the last paragraph of L above for an example how
106             to do this during CI builds only.
107              
108             =head2 Validations provided by this module
109              
110             =head3 no_indirect_method_overrides
111              
112             There are many methods within DBIC which are
113             L<"strictly sugar"|DBIx::Class::MethodAttributes/DBIC_method_is_indirect_sugar>
114             and should never be overridden by your application (e.g. see warnings at the
115             end of L and L).
116             Starting with C DBIC is much more aggressive in calling the
117             underlying non-sugar methods directly, which in turn means that almost all
118             user-side overrides of sugar methods are never going to be invoked. These
119             situations are now reliably detected and reported individually (you may
120             end up with a lot of output on C due to this).
121              
122             Note: B reported by this check B<*MUST*> be resolved
123             before upgrading DBIC in production. Malfunctioning business logic and/or
124             B may result otherwise.
125              
126             =head3 valid_c3_composition
127              
128             Looks through everything returned by L, and
129             for any class that B already utilize L a
130             L is calculated and then
131             compared to the shadowing map as if C was requested in the first place.
132             Any discrepancies are reported in order to clearly identify L
133             bugs|https://blog.afoolishmanifesto.com/posts/mros-and-you> especially when
134             encountered within complex inheritance hierarchies.
135              
136             =head3 no_inheritance_crosscontamination
137              
138             Checks that every individual L,
139             L, L,
140             L
141             and L class does not inherit from
142             an unexpected DBIC base class: e.g. an error will be raised if your
143             C inherits from both C and
144             C.
145              
146             =head1 METHODS
147              
148             =head2 perform_schema_sanity_checks
149              
150             =over
151              
152             =item Arguments: L<$schema|DBIx::Class::Schema>
153              
154             =item Return Value: unspecified (ignored by caller)
155              
156             =back
157              
158             The entry point expected by the
159             L. See
160             L for details.
161              
162             =cut
163              
164             sub perform_schema_sanity_checks {
165 2     2 1 6 my ($self, $schema) = @_;
166              
167             local $DBIx::Class::_Util::describe_class_query_cache->{'!internal!'} = {}
168             if
169             # does not make a measurable difference on 5.10+
170             DBIx::Class::_ENV_::OLD_MRO
171             and
172             # the callstack shouldn't really be recursive, but for completeness...
173 2         3 ! $DBIx::Class::_Util::describe_class_query_cache->{'!internal!'}
174             ;
175              
176 2         4 my (@errors_found, $schema_desc);
177 2         3 for my $ch ( @{ $self->available_checks } ) {
  2         5  
178              
179 6         12 my $err = $self->${\"check_$ch"} ( $schema );
  6         35  
180              
181             push @errors_found, map
182             {
183             {
184 1 50 33     35 check_name => $ch,
185             formatted_error => $_,
186             schema_desc => ( $schema_desc ||=
187             ( length ref $schema )
188             ? refdesc $schema
189             : "'$schema'"
190             ),
191             }
192             }
193             @{
194 6 50       30 $self->${\"format_${ch}_errors"} ( $err )
  1 100       4  
  1         16  
195             ||
196             []
197             }
198             if @$err;
199             }
200              
201 2 100       15 $self->emit_errors(\@errors_found)
202             if @errors_found;
203             }
204              
205             =head2 available_checks
206              
207             =over
208              
209             =item Arguments: none
210              
211             =item Return Value: \@list_of_check_names
212              
213             =back
214              
215             The list of checks L will perform on the
216             provided L<$schema|DBIx::Class::Schema> object. For every entry returned
217             by this method, there must be a pair of I> and
218             I> methods available.
219              
220             Override this method to add checks to the
221             L.
222              
223             =cut
224              
225 3     3 1 122 sub available_checks { [qw(
226             valid_c3_composition
227             no_inheritance_crosscontamination
228             no_indirect_method_overrides
229             )] }
230              
231             =head2 emit_errors
232              
233             =over
234              
235             =item Arguments: \@list_of_formatted_errors
236              
237             =item Return Value: unspecified (ignored by caller)
238              
239             =back
240              
241             Takes an array reference of individual errors returned by various
242             I> formatters, and outputs them on C.
243              
244             This method is the most convenient integration point for a 3rd party logging
245             framework.
246              
247             Each individual error is expected to be a hash reference with all values being
248             plain strings as follows:
249              
250             {
251             schema_desc => $human_readable_description_of_the_passed_in_schema
252             check_name => $name_of_the_check_as_listed_in_available_checks()
253             formatted_error => $error_text_as_returned_by_format_$checkname_errors()
254             }
255              
256             If the environment variable C is set to
257             a true value this method will throw an exception with the same text. Those who
258             prefer to take no chances could set this variable permanently as part of their
259             deployment scripts.
260              
261             =cut
262              
263             # *NOT* using carp_unique and the warn framework - make
264             # it harder to accidentaly silence problems via $SIG{__WARN__}
265             sub emit_errors {
266             #my ($self, $errs) = @_;
267              
268             my @final_error_texts = map {
269             sprintf( "Schema %s failed the '%s' sanity check: %s\n",
270 1         3 @{$_}{qw( schema_desc check_name formatted_error )}
  1         13  
271             );
272 1     1 1 2 } @{$_[1]};
  1         2  
273              
274             emit_loud_diag(
275             msg => $_
276 1         6 ) for @final_error_texts;
277              
278             # Do not use the constant - but instead check the env every time
279             # This will allow people to start auditing their apps piecemeal
280             DBIx::Class::Exception->throw( join "\n", @final_error_texts, ' ' )
281 1 50       8 if $ENV{DBIC_ASSERT_NO_FAILING_SANITY_CHECKS};
282             }
283              
284             =head2 all_schema_related_classes
285              
286             =over
287              
288             =item Arguments: L<$schema|DBIx::Class::Schema>
289              
290             =item Return Value: @sorted_list_of_unique_class_names
291              
292             =back
293              
294             This is a convenience method providing a list (not an arrayref) of
295             "interesting classes" related to the supplied schema. The returned list
296             currently contains the following class names:
297              
298             =over
299              
300             =item * The L class itself
301              
302             =item * The associated L class if any
303              
304             =item * The classes of all L if any
305              
306             =item * All L classes for all registered ResultSource instances
307              
308             =item * All L classes for all registered ResultSource instances
309              
310             =back
311              
312             =cut
313              
314             sub all_schema_related_classes {
315 4     4 1 12 my ($self, $schema) = @_;
316              
317             sort( uniq( map {
318 14 100       86 ( not defined $_ ) ? ()
    50          
319             : ( defined blessed $_ ) ? ref $_
320             : $_
321             } (
322             $schema,
323             $schema->storage,
324             ( map {
325 2         40 $_,
326             $_->result_class,
327             $_->resultset_class,
328 4         98 } map { $schema->source($_) } $schema->sources ),
  2         36  
329             )));
330             }
331              
332              
333             sub format_no_indirect_method_overrides_errors {
334             # my ($self, $errors) = @_;
335              
336             [ map { sprintf(
337             "Method(s) %s override the convenience shortcut %s::%s(): "
338             . 'it is almost certain these overrides *MAY BE COMPLETELY IGNORED* at '
339             . 'runtime. You MUST reimplement each override to hook a method from the '
340             . "chain of calls within the convenience shortcut as seen when running:\n "
341             . '~$ perl -M%2$s -MDevel::Dwarn -e "Ddie { %3$s => %2$s->can(q(%3$s)) }"',
342 0         0 join (', ', map { "$_()" } sort @{ $_->{by} } ),
  0         0  
343             $_->{overridden}{via_class},
344             $_->{overridden}{name},
345 0     0 0 0 )} @{ $_[1] } ]
  0         0  
  0         0  
346             }
347              
348             sub check_no_indirect_method_overrides {
349 2     2 0 8 my ($self, $schema) = @_;
350              
351 2         7 my( @err, $seen_shadowing_configurations );
352              
353             METHOD_STACK:
354 2         13 for my $method_stack ( map {
355 7 50       15 values %{ describe_class_methods($_)->{methods_with_supers} || {} }
  7         23  
356             } $self->all_schema_related_classes($schema) ) {
357              
358 87         101 my $nonsugar_methods;
359              
360 87         119 for (@$method_stack) {
361              
362             push @$nonsugar_methods, $_ and next
363             unless(
364             $_->{attributes}{DBIC_method_is_indirect_sugar}
365             or
366             $_->{attributes}{DBIC_method_is_generated_from_resultsource_metadata}
367 176 50 50     572 );
      33        
368              
369             push @err, {
370             overridden => {
371             name => $_->{name},
372             via_class => (
373             # this way we report a much better Dwarn oneliner in the error
374             $_->{attributes}{DBIC_method_is_bypassable_resultsource_proxy}
375             ? 'DBIx::Class::ResultSource'
376             : $_->{via_class}
377             ),
378             },
379 0         0 by => [ map { "$_->{via_class}::$_->{name}" } @$nonsugar_methods ],
380             } if (
381             $nonsugar_methods
382             and
383             ! $seen_shadowing_configurations->{
384             join "\0",
385             map
386 0 0 0     0 { refaddr $_ }
  0 0       0  
387             (
388             $_,
389             @$nonsugar_methods,
390             )
391             }++
392             )
393             ;
394              
395 0         0 next METHOD_STACK;
396             }
397             }
398              
399             \@err
400 2         13 }
401              
402              
403             sub format_valid_c3_composition_errors {
404             # my ($self, $errors) = @_;
405              
406             [ map { sprintf(
407             "Class '%s' %s using the '%s' MRO affecting the lookup order of the "
408             . "following method(s): %s. You MUST add the following line to '%1\$s' "
409             . "right after strict/warnings:\n use mro 'c3';",
410             $_->{class},
411             ( ($_->{initial_mro} eq $_->{current_mro}) ? 'is' : 'was originally' ),
412             $_->{initial_mro},
413 1 50       8 join (', ', map { "$_()" } sort keys %{$_->{affected_methods}} ),
  3         21  
  1         14  
414 1     1 0 2 )} @{ $_[1] } ]
  1         4  
415             }
416              
417              
418             my $base_ISA = {
419             map { $_ => 1 } @{mro::get_linear_isa("DBIx::Class")}
420             };
421              
422             sub check_valid_c3_composition {
423 2     2 0 5 my ($self, $schema) = @_;
424              
425 2         4 my @err;
426              
427             #
428             # A *very* involved check, to absolutely minimize false positives
429             # If this check returns an issue - it *better be* a real one
430             #
431 2         5 for my $class ( $self->all_schema_related_classes($schema) ) {
432              
433 7         15 my $desc = do {
434 5     5   5883 no strict 'refs';
  5         14  
  5         2917  
435             describe_class_methods({
436             class => $class,
437 7         68 ( ${"${class}::__INITIAL_MRO_UPON_DBIC_LOAD__"}
438 7 100       195 ? ( use_mro => ${"${class}::__INITIAL_MRO_UPON_DBIC_LOAD__"} )
  3         20  
439             : ()
440             ),
441             })
442             };
443              
444             # is there anything to check?
445             next unless (
446             ! $desc->{mro}{is_c3}
447             and
448             $desc->{methods_with_supers}
449             and
450             my @potentially_problematic_method_stacks =
451             grep
452             {
453             # at least 2 variants came via inheritance (not ours)
454             (
455 64         177 (grep { $_->{via_class} ne $class } @$_)
456             >
457             1
458             )
459             and
460             #
461             # last ditch effort to skip examining an alternative mro
462             # IFF the entire "foreign" stack is located in the "base isa"
463             #
464             # This allows for extra efficiency (as there are several
465             # with_supers methods that would always be there), but more
466             # importantly saves one from tripping on the nonsensical yet
467             # begrudgingly functional (as in - no adverse effects):
468             #
469             # use base 'DBIx::Class';
470             # use base 'DBIx::Class::Schema';
471             #
472             (
473             grep {
474             # not ours
475 31 100       50 $_->{via_class} ne $class
476             and
477             # not from the base stack either
478             ! $base_ISA->{$_->{via_class}}
479 62 50       233 } @$_
480             )
481             }
482 7 100 66     79 values %{ $desc->{methods_with_supers} }
  5   66     23  
483             );
484              
485 1         2 my $affected_methods;
486              
487 1         3 for my $stack (@potentially_problematic_method_stacks) {
488              
489             # If we got so far - we need to see what the class would look
490             # like under c3 and compare, sigh
491             #
492             # Note that if the hierarchy is *really* fucked (like the above
493             # double-base e.g.) then recalc under 'c3' WILL FAIL, hence the
494             # extra eval: if we fail we report things as "jumbled up"
495             #
496             $affected_methods->{$stack->[0]{name}} = [
497 7         38 map { $_->{via_class} } @$stack
498             ] unless dbic_internal_try {
499              
500             serialize($stack)
501             eq
502             serialize(
503             describe_class_methods({ class => $class, use_mro => 'c3' })
504             ->{methods}
505             ->{$stack->[0]{name}}
506             )
507 10 100   10   71 };
  10         24  
508             }
509              
510             push @err, {
511             class => $class,
512             initial_linear_isa => $desc->{linear_isa},
513 1         3 current_linear_isa => do { (undef, my @isa) = @{ mro::get_linear_isa($class) }; \@isa },
  1         9  
  1         18  
514             initial_mro => $desc->{mro}{type},
515 1 50       7 current_mro => mro::get_mro($class),
516             affected_methods => $affected_methods,
517             } if $affected_methods;
518             }
519              
520 2         20 \@err;
521             }
522              
523              
524             sub format_no_inheritance_crosscontamination_errors {
525             # my ($self, $errors) = @_;
526              
527             [ map { sprintf(
528             "Class '%s' registered in the role of '%s' unexpectedly inherits '%s': "
529             . 'you must resolve this by either removing an erroneous `use base` call '
530             . "or switching to Moo(se)-style delegation (i.e. the 'handles' keyword)",
531             $_->{class},
532             $_->{type},
533             $_->{unexpectedly_inherits},
534 0     0 0 0 )} @{ $_[1] } ]
  0         0  
  0         0  
535             }
536              
537             sub check_no_inheritance_crosscontamination {
538 2     2 0 9 my ($self, $schema) = @_;
539              
540 2         5 my @err;
541              
542             my $to_check = {
543             Schema => [ $schema ],
544             Storage => [ $schema->storage ],
545 2         125 ResultSource => [ map { $schema->source($_) } $schema->sources ],
  1         24  
546             };
547              
548             $to_check->{ResultSet} = [
549 2         22 map { $_->resultset_class } @{$to_check->{ResultSource}}
  1         17  
  2         8  
550             ];
551              
552             $to_check->{Core} = [
553 2         6 map { $_->result_class } @{$to_check->{ResultSource}}
  1         16  
  2         8  
554             ];
555              
556             # Reduce everything to a unique sorted list of class names
557             $_ = [ sort( uniq( map {
558 7 100       41 ( not defined $_ ) ? ()
    50          
559             : ( defined blessed $_ ) ? ref $_
560             : $_
561 2         10 } @$_ ) ) ] for values %$to_check;
562              
563 2         10 for my $group ( sort keys %$to_check ) {
564 10         14 for my $class ( @{ $to_check->{$group} } ) {
  10         19  
565 7         15 for my $foreign_base (
566 28         68 map { "DBIx::Class::$_" } sort grep { $_ ne $group } keys %$to_check
  35         66  
567             ) {
568              
569 28 0       170 push @err, {
    50          
570             class => $class,
571             type => ( $group eq 'Core' ? 'ResultClass' : $group ),
572             unexpectedly_inherits => $foreign_base
573             } if $class->isa($foreign_base);
574             }
575             }
576             }
577              
578 2         14 \@err;
579             }
580              
581             1;
582              
583             __END__