Commit | Line | Data |
12e7015a |
1 | package DBIx::Class::Schema::SanityChecker; |
2 | |
3 | use strict; |
4 | use warnings; |
5 | |
6 | use DBIx::Class::_Util qw( |
7 | dbic_internal_try refdesc uniq serialize |
8 | describe_class_methods emit_loud_diag |
9 | ); |
10 | use DBIx::Class (); |
12e7015a |
11 | use Scalar::Util qw( blessed refaddr ); |
12 | use namespace::clean; |
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 | |
58c8eea0 |
23 | # this is the default setting |
12e7015a |
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<validation framework|DBIx::Class::Schema/schema_sanity_checker>. |
31 | |
58c8eea0 |
32 | The validator is B<enabled by default>. See L</Performance considerations> |
33 | for discussion of the runtime effects. |
12e7015a |
34 | |
35 | Use of this class begins by invoking L</perform_schema_sanity_checks> |
36 | (usually via L<DBIx::Class::Schema/connection>), which in turn starts |
37 | invoking validators I<C<check_$checkname()>> in the order listed in |
38 | L</available_checks>. For each set of returned errors (if any) |
39 | I<C<format_$checkname_errors()>> is called and the resulting strings are |
40 | passed to L</emit_errors>, where final headers are prepended and the entire |
41 | thing is printed on C<STDERR>. |
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<isn't free|/Performance considerations>). 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<RT#107462|https://rt.cpan.org/Ticket/Display.html?id=107462>, |
57 | L<RT#114440|https://rt.cpan.org/Ticket/Display.html?id=114440>, 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<silent breakages>. |
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<should be investigated>. |
67 | |
68 | =head2 Performance considerations |
69 | |
70 | First of all - after your connection has been established - there is B<no |
71 | runtime penalty> whenever the checks are enabled. |
72 | |
73 | By default the checks are triggered every time |
74 | L<DBIx::Class::Schema/connection> 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<DBIx::Class::Helpers> C<v2.032002> 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<t/lib/DBICTest/BaseSchema.pm>: |
85 | |
86 | ~/dbic_repo$ git show 39636786 | perl -ne "print if 16..61" |
87 | |
88 | Whatever you do, B<please do not disable the checks entirely>: it is not |
89 | worth the risk. |
90 | |
91 | =head3 Perl5.8 |
92 | |
93 | The situation with perl interpreters before C<v5.10.0> is sadly more |
94 | complicated: due to lack of built-in L<pluggable mro support|mro>, the |
95 | mechanism used to interrogate various classes is |
96 | L<< B<much> slower|https://github.com/dbsrgits/dbix-class/commit/296248c3 >>. |
97 | As a result the very same version of L<DBIx::Class::Helpers> |
98 | L<mentioned above|/Performance considerations> takes a C<B<220%>> hit on its |
99 | test execution time (these numbers are observed with the speedups of |
100 | L<Class::C3::XS> available, without them the slowdown reaches the whopping |
101 | C<350%>). |
102 | |
12e7015a |
103 | It is the author's B<strongest> 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</Performance considerations> 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<DBIx::Class::ResultSet/create> and L<DBIx::Class::Schema/connect>). |
116 | Starting with C<v0.082900> 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<STDERR> due to this). |
121 | |
122 | Note: B<ANY AND ALL ISSUES> reported by this check B<*MUST*> be resolved |
123 | before upgrading DBIC in production. Malfunctioning business logic and/or |
124 | B<SEVERE DATA LOSS> may result otherwise. |
125 | |
126 | =head3 valid_c3_composition |
127 | |
128 | Looks through everything returned by L</all_schema_related_classes>, and |
129 | for any class that B<does not> already utilize L<c3 MRO|mro/The C3 MRO> a |
130 | L<method shadowing map|App::Isa::Splain/SYNOPSIS> is calculated and then |
131 | compared to the shadowing map as if C<c3 MRO> was requested in the first place. |
132 | Any discrepancies are reported in order to clearly identify L<hard to explain |
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<Schema|DBIx::Class::Schema>, |
139 | L<Storage|DBIx::Class::Storage>, L<ResultSource|DBIx::Class::ResultSource>, |
140 | L<ResultSet|DBIx::Class::ResultSet> |
141 | and L<Result|DBIx::Class::Manual::ResultClass> class does not inherit from |
142 | an unexpected DBIC base class: e.g. an error will be raised if your |
143 | C<MyApp::Schema> inherits from both C<DBIx::Class::Schema> and |
144 | C<DBIx::Class::ResultSet>. |
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<validation framework|DBIx::Class::Schema/schema_sanity_checker>. See |
160 | L</DESCRIPTION> for details. |
161 | |
162 | =cut |
163 | |
164 | sub perform_schema_sanity_checks { |
165 | 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 | ! $DBIx::Class::_Util::describe_class_query_cache->{'!internal!'} |
174 | ; |
175 | |
176 | my (@errors_found, $schema_desc); |
177 | for my $ch ( @{ $self->available_checks } ) { |
178 | |
179 | my $err = $self->${\"check_$ch"} ( $schema ); |
180 | |
181 | push @errors_found, map |
182 | { |
183 | { |
184 | 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 | $self->${\"format_${ch}_errors"} ( $err ) |
195 | || |
196 | [] |
197 | } |
198 | if @$err; |
199 | } |
200 | |
201 | $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</perform_schema_sanity_checks> 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<C<check_$checkname()>> and |
218 | I<C<format_$checkname_errors()>> methods available. |
219 | |
220 | Override this method to add checks to the |
221 | L<currently available set|/Validations provided by this module>. |
222 | |
223 | =cut |
224 | |
225 | 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<C<format_$checkname_errors()>> formatters, and outputs them on C<STDERR>. |
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<DBIC_ASSERT_NO_FAILING_SANITY_CHECKS> 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 | @{$_}{qw( schema_desc check_name formatted_error )} |
271 | ); |
272 | } @{$_[1]}; |
273 | |
274 | emit_loud_diag( |
275 | msg => $_ |
276 | ) 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 | 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<Schema|DBIx::Class::Schema> class itself |
301 | |
302 | =item * The associated L<Storage|DBIx::Class::Schema/storage> class if any |
303 | |
304 | =item * The classes of all L<registered ResultSource instances|DBIx::Class::Schema/sources> if any |
305 | |
306 | =item * All L<Result|DBIx::Class::ResultSource/result_class> classes for all registered ResultSource instances |
307 | |
308 | =item * All L<ResultSet|DBIx::Class::ResultSource/resultset_class> classes for all registered ResultSource instances |
309 | |
310 | =back |
311 | |
312 | =cut |
313 | |
314 | sub all_schema_related_classes { |
315 | my ($self, $schema) = @_; |
316 | |
317 | sort( uniq( map { |
318 | ( not defined $_ ) ? () |
319 | : ( defined blessed $_ ) ? ref $_ |
320 | : $_ |
321 | } ( |
322 | $schema, |
323 | $schema->storage, |
324 | ( map { |
325 | $_, |
326 | $_->result_class, |
327 | $_->resultset_class, |
328 | } map { $schema->source($_) } $schema->sources ), |
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 | join (', ', map { "$_()" } sort @{ $_->{by} } ), |
2781bf35 |
343 | $_->{overridden}{via_class}, |
344 | $_->{overridden}{name}, |
12e7015a |
345 | )} @{ $_[1] } ] |
346 | } |
347 | |
348 | sub check_no_indirect_method_overrides { |
349 | my ($self, $schema) = @_; |
350 | |
351 | my( @err, $seen_shadowing_configurations ); |
352 | |
353 | METHOD_STACK: |
354 | for my $method_stack ( map { |
355 | values %{ describe_class_methods($_)->{methods_with_supers} || {} } |
356 | } $self->all_schema_related_classes($schema) ) { |
357 | |
358 | my $nonsugar_methods; |
359 | |
360 | for (@$method_stack) { |
361 | |
362 | push @$nonsugar_methods, $_ and next |
363 | unless $_->{attributes}{DBIC_method_is_indirect_sugar}; |
364 | |
365 | push @err, { |
2781bf35 |
366 | overridden => { |
12e7015a |
367 | name => $_->{name}, |
28ef9468 |
368 | via_class => ( |
369 | # this way we report a much better Dwarn oneliner in the error |
370 | $_->{attributes}{DBIC_method_is_bypassable_resultsource_proxy} |
371 | ? 'DBIx::Class::ResultSource' |
372 | : $_->{via_class} |
373 | ), |
12e7015a |
374 | }, |
375 | by => [ map { "$_->{via_class}::$_->{name}" } @$nonsugar_methods ], |
376 | } if ( |
377 | $nonsugar_methods |
378 | and |
379 | ! $seen_shadowing_configurations->{ |
380 | join "\0", |
381 | map |
382 | { refaddr $_ } |
383 | ( |
384 | $_, |
385 | @$nonsugar_methods, |
386 | ) |
387 | }++ |
388 | ) |
389 | ; |
390 | |
391 | next METHOD_STACK; |
392 | } |
393 | } |
394 | |
395 | \@err |
396 | } |
397 | |
398 | |
399 | sub format_valid_c3_composition_errors { |
400 | # my ($self, $errors) = @_; |
401 | |
402 | [ map { sprintf( |
403 | "Class '%s' %s using the '%s' MRO affecting the lookup order of the " |
404 | . "following method(s): %s. You MUST add the following line to '%1\$s' " |
405 | . "right after strict/warnings:\n use mro 'c3';", |
406 | $_->{class}, |
407 | ( ($_->{initial_mro} eq $_->{current_mro}) ? 'is' : 'was originally' ), |
408 | $_->{initial_mro}, |
409 | join (', ', map { "$_()" } sort keys %{$_->{affected_methods}} ), |
410 | )} @{ $_[1] } ] |
411 | } |
412 | |
413 | |
414 | my $base_ISA = { |
415 | map { $_ => 1 } @{mro::get_linear_isa("DBIx::Class")} |
416 | }; |
417 | |
418 | sub check_valid_c3_composition { |
419 | my ($self, $schema) = @_; |
420 | |
421 | my @err; |
422 | |
423 | # |
424 | # A *very* involved check, to absolutely minimize false positives |
425 | # If this check returns an issue - it *better be* a real one |
426 | # |
427 | for my $class ( $self->all_schema_related_classes($schema) ) { |
428 | |
429 | my $desc = do { |
430 | no strict 'refs'; |
431 | describe_class_methods({ |
432 | class => $class, |
433 | ( ${"${class}::__INITIAL_MRO_UPON_DBIC_LOAD__"} |
434 | ? ( use_mro => ${"${class}::__INITIAL_MRO_UPON_DBIC_LOAD__"} ) |
435 | : () |
436 | ), |
437 | }) |
438 | }; |
439 | |
440 | # is there anything to check? |
441 | next unless ( |
442 | ! $desc->{mro}{is_c3} |
443 | and |
444 | $desc->{methods_with_supers} |
445 | and |
446 | my @potentially_problematic_method_stacks = |
447 | grep |
448 | { |
449 | # at least 2 variants came via inheritance (not ours) |
450 | ( |
451 | (grep { $_->{via_class} ne $class } @$_) |
452 | > |
453 | 1 |
454 | ) |
455 | and |
456 | # |
457 | # last ditch effort to skip examining an alternative mro |
458 | # IFF the entire "foreign" stack is located in the "base isa" |
459 | # |
460 | # This allows for extra efficiency (as there are several |
461 | # with_supers methods that would always be there), but more |
462 | # importantly saves one from tripping on the nonsensical yet |
463 | # begrudgingly functional (as in - no adverse effects): |
464 | # |
465 | # use base 'DBIx::Class'; |
466 | # use base 'DBIx::Class::Schema'; |
467 | # |
468 | ( |
469 | grep { |
470 | # not ours |
471 | $_->{via_class} ne $class |
472 | and |
473 | # not from the base stack either |
474 | ! $base_ISA->{$_->{via_class}} |
475 | } @$_ |
476 | ) |
477 | } |
478 | values %{ $desc->{methods_with_supers} } |
479 | ); |
480 | |
481 | my $affected_methods; |
482 | |
483 | for my $stack (@potentially_problematic_method_stacks) { |
484 | |
485 | # If we got so far - we need to see what the class would look |
486 | # like under c3 and compare, sigh |
487 | # |
488 | # Note that if the hierarchy is *really* fucked (like the above |
489 | # double-base e.g.) then recalc under 'c3' WILL FAIL, hence the |
490 | # extra eval: if we fail we report things as "jumbled up" |
491 | # |
492 | $affected_methods->{$stack->[0]{name}} = [ |
493 | map { $_->{via_class} } @$stack |
494 | ] unless dbic_internal_try { |
495 | |
496 | serialize($stack) |
497 | eq |
498 | serialize( |
499 | describe_class_methods({ class => $class, use_mro => 'c3' }) |
500 | ->{methods} |
501 | ->{$stack->[0]{name}} |
502 | ) |
503 | }; |
504 | } |
505 | |
506 | push @err, { |
507 | class => $class, |
3aa25d8b |
508 | initial_linear_isa => $desc->{linear_isa}, |
509 | current_linear_isa => do { (undef, my @isa) = @{ mro::get_linear_isa($class) }; \@isa }, |
12e7015a |
510 | initial_mro => $desc->{mro}{type}, |
511 | current_mro => mro::get_mro($class), |
512 | affected_methods => $affected_methods, |
513 | } if $affected_methods; |
514 | } |
515 | |
516 | \@err; |
517 | } |
518 | |
519 | |
520 | sub format_no_inheritance_crosscontamination_errors { |
521 | # my ($self, $errors) = @_; |
522 | |
523 | [ map { sprintf( |
524 | "Class '%s' registered in the role of '%s' unexpectedly inherits '%s': " |
525 | . 'you must resolve this by either removing an erroneous `use base` call ' |
526 | . "or switching to Moo(se)-style delegation (i.e. the 'handles' keyword)", |
527 | $_->{class}, |
528 | $_->{type}, |
529 | $_->{unexpectedly_inherits}, |
530 | )} @{ $_[1] } ] |
531 | } |
532 | |
533 | sub check_no_inheritance_crosscontamination { |
534 | my ($self, $schema) = @_; |
535 | |
536 | my @err; |
537 | |
538 | my $to_check = { |
539 | Schema => [ $schema ], |
540 | Storage => [ $schema->storage ], |
541 | ResultSource => [ map { $schema->source($_) } $schema->sources ], |
542 | }; |
543 | |
544 | $to_check->{ResultSet} = [ |
545 | map { $_->resultset_class } @{$to_check->{ResultSource}} |
546 | ]; |
547 | |
548 | $to_check->{Core} = [ |
549 | map { $_->result_class } @{$to_check->{ResultSource}} |
550 | ]; |
551 | |
552 | # Reduce everything to a unique sorted list of class names |
553 | $_ = [ sort( uniq( map { |
554 | ( not defined $_ ) ? () |
555 | : ( defined blessed $_ ) ? ref $_ |
556 | : $_ |
557 | } @$_ ) ) ] for values %$to_check; |
558 | |
559 | for my $group ( sort keys %$to_check ) { |
560 | for my $class ( @{ $to_check->{$group} } ) { |
561 | for my $foreign_base ( |
562 | map { "DBIx::Class::$_" } sort grep { $_ ne $group } keys %$to_check |
563 | ) { |
564 | |
565 | push @err, { |
566 | class => $class, |
567 | type => ( $group eq 'Core' ? 'ResultClass' : $group ), |
568 | unexpectedly_inherits => $foreign_base |
569 | } if $class->isa($foreign_base); |
570 | } |
571 | } |
572 | } |
573 | |
574 | \@err; |
575 | } |
576 | |
577 | 1; |
578 | |
579 | __END__ |
580 | |
581 | =head1 FURTHER QUESTIONS? |
582 | |
583 | Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>. |
584 | |
585 | =head1 COPYRIGHT AND LICENSE |
586 | |
587 | This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE> |
588 | by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can |
589 | redistribute it and/or modify it under the same terms as the |
590 | L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>. |