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